Point the Zorin OS-provided user directories to directories on an external drive

Binding

The Zorin OS-provided user directories should be empty for this, as bind-mounting them hides the already-existing contents (the content's not gone, you just can't access it, because when you click on that directory, it takes you to the external drive's directory).

  1. First, on the external drive, create the Documents, Music, Pictures and Videos directories.

  2. Find the paths to each of those directories.

For the Zorin OS-provided user directories:

/home/$USER/Documents
/home/$USER/Music
/home/$USER/Pictures
/home/$USER/Videos

For the external drive directories (let's assume your drive is named OldRB):

/media/$USER/OldRB/Documents
/media/$USER/OldRB/Music
/media/$USER/OldRB/Pictures
/media/$USER/OldRB/Videos
  1. Issue the commands to create the bindings (my $USER username is 'owner'):

sudo mount --bind --verbose /home/$USER/Documents /media/$USER/OldRB/Documents

Returns:
mount: /home/owner/Documents bound on /media/owner/OldRB/Documents.


sudo mount --bind --verbose /home/$USER/Music /media/$USER/OldRB/Music

Returns:
mount: /home/owner/Music bound on /media/owner/OldRB/Music.


sudo mount --bind --verbose /home/$USER/Pictures /media/$USER/OldRB/Pictures

Returns:
mount: /home/owner/Pictures bound on /media/owner/OldRB/Pictures.


sudo mount --bind --verbose /home/$USER/Videos /media/$USER/OldRB/Videos

Returns:
mount: /home/owner/Videos bound on /media/owner/OldRB/Videos


Now, when you navigate to, for instance, the Documents directory (/home/$USER/Documents), you're really navigating to the Documents directory on the external drive (/media/$USER/OldRB/Documents).

If you want to unmount, do:

sudo umount /media/$USER/OldRB/Documents /home/$USER/Documents
sudo umount /media/$USER/OldRB/Music /home/$USER/Music
sudo umount /media/$USER/OldRB/Pictures /home/$USER/Pictures
sudo umount /media/$USER/OldRB/Videos /home/$USER/Videos


If you've right-clicked the taskbar, selected Zorin Appearance, gone to the Desktop side-tab, enabled "Icons on Desktop", and enabled "Mounted Volumes", those bind-mounts will now also show up on your desktop.

You can change the icon of each mount by right-clicking it, selecting Properties, clicking the icon in the dialog box that pops up, then navigating to /usr/share/icons and finding a suitable icon (I use the 48x48 sized icons in the image below... they're automatically scaled to the icon size you've set).


The above doesn't persist across reboots... I'm working on it.

[EDIT 1]
OK, I got it. It was far more complicated than it should be. The problem is systemd.

Systemd ignores all fstab options when mounting a bind-mount, so to get it to work, one has to mount it twice (as sysvinit did). Unfortunately, systemd also enforces uniqueness of mount points (you can't mount the same directory twice)... so with systemd, it would seem that we're stuck, unable to use bind-mount in fstab.

We could create a script that is put into Zorin menu > System Tools > Startup Applications that does the mounting on each reboot as I did above, but if you set up custom icons (icons are in /usr/share/icons) for each of the bind-mounts, those aren't saved across reboots doing it that way.

So we're going to create a systemd generator that runs alongside the existing systemd generator used for mounting. Fortunately, most of the work has already been done for us.

Create a file, name it bindmount-generator.

Put the following text into the file:


#!/bin/sh
#
# Copyright (c) 2015 Christian Seiler <christian@iwakd.de>
# https://blog.iwakd.de/sites/default/files/bindmount-generator.txt
#
# License: MIT, <http://opensource.org/licenses/MIT>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Rationale
# ---------
# This is needed because when a bind-mount is first created, all specified
# options will be ignored, i.e. the mountpoint options will be a duplicate of
# the options of the source mountpoint.
#
# Only if you remount the mountpoint will the options be applied.
#
# Therefore, if you add a bind mount to /etc/fstab, the options specified
# alongside bind will not be applied on boot. In sysvinit systems, one could
# add multiple mount points with the same target to cause 'mount -a' to remount
# it at the second line, but systemd enforces uniqueness of mount points, i.e.
# you cannot specify the same directory twice.
#
# This generator is a way to work around this problem. It parses /etc/fstab (in
# parallel to systemd's own fstab-generator) and writes out small snippets of
# systemd configuration that cause the bind mounts specified in /etc/fstab to
# be remounted immediately after their initial mounting.
#
# The idea for the generated units is based what is described in [1], but the
# implementation is much more generic.
# [1] https://web.archive.org/web/20160425202316/http://www.sagui.org/~gustavo/blog/fedora/read-only-bind-mount-with-systemd.html
#
# Note that this is implemented as a shell script for now, but would be more
# efficient as a C binary.
#
# Basic logic
# -----------
#
#	1.	generate two service units,
#		bindmount-local@.service and bindmount-remote@.service
#		These services just remount the filesystem, causing the mount points to
#		acquire the options in /etc/fstab. The service is implemented as a
#		oneshot service, so it immediately goes back to inactive after it
#		completes its actions, allowing it to be pulled back in if the mount
#		unit is to be started again at a later point in time.
#
#	2.	read in /etc/fstab, look for bind mounts (that are not recursive binds)
#
#	3.	generate a drop-in configuration for each of these bind mounts to make
#		sure they pull in (via Require=) bindmount-$type@$path.service
#
#	The dependencies have the following consequences:
#
#	-	drop-in: Requires=bindmount-$type@$path.service
#		->	if the mount is to be started, it will pull in our generated service
#	-	service: After=%i.mount
#		->	the service is ordered after the mount itself, i.e. we remount the
#			mount point only after it was initially mounted
#	-	service: Before=local-fs.target (or remote-fs.target)
#		->	make sure that remounting is done before any services are started
#			that might need the filesystems (services are ordered after either
#			local-fs.target or remote-fs.target)
#
# Preconditions
# -------------
#
# Note: this generator is executed by systemd VERY early in the boot process.
#
# First of all: logging is only possible to /dev/kmsg (we redirect stdout and
# stderr there first thing). We also try to log as little as possible since
# this generator will be executed every time systemctl daemon-reload is called.
#
# / is still read-only, /tmp is not yet mounted when this is called for the
# first time. IMPORTANT: Heredoc support in most shells requires temporary
# files, so use lots of regular echos.
#
# This is run in parallel to all other generators, so we can't rely on them
# being done yet.
#
# In some containers we don't have /dev/kmsg. This means we won't be able to
# log anything there, but before our logic creates a regular file in the /dev
# filesystem, just forward messages to /dev/null if file doesn't exist.
# Not nice, but no other option for us.
if [ -c "/dev/kmsg" ] ; then
	logdevice="/dev/kmsg"
else
	logdevice="/dev/null"
fi

# redirect stdout + stderr to the log device
exec >"$logdevice" 2>&1

# this should work on all distros, /usr-merged or not, but if you have a
# /usr-merged distro and it doesn't have a compat symlink for /bin (or 
# /bin/mount), just change this accordingly
MOUNT_EXE=/bin/mount

# we need two of these units that are basically identical, but with different
# dependencies, so make a function out of it
generate_unit()
{
	# Note that since we always order after the mount unit, we don't need to
	# take care of the dependencies of the mount unit # itself. The only tricky
	# thing is the Before= dependency on the filesystem target, which is why we
	# need two units.
	echo "[Unit]"
	echo "Description=Remouning bind mount with proper fstab options"
	echo "DefaultDependencies=no"
	echo "After=%i.mount"
	echo "Before=$1"
	echo ""
	echo "[Service]"
	echo "Type=oneshot"
	echo "ExecStart=${MOUNT_EXE} -o remount,bind /%I"
}

# determine if a fstype is network filesystem (typically fstype should be none,
# so if this really is a on a network fs, the user should specify _netdev, but
# just in case...)
is_network_filesystem()
{
	# systemd's logic: strip fuse, check for a hard-coded list of file systems
	if [ x"${1#fuse.}"x != x"$1"x ] ; then
		is_network_filesystem "${1#fuse.}"
		return $?
	fi
	case "$1" in
		cifs|smbfs|sshfs|ncpfs|ncp|nfs|nfs4|gfs|gfs2|glusterfs)
			return 0 ;;
		*)
			return 1 ;;
	esac
}

# kill multiple slashes in path (systemd does no other normalization)
path_kill_slashes()
{
	echo "$1" | sed 's%//*%/%g'
}

# systemd-escape implementation
sdescape()
{
	# requires systemd-216
	# (Debian Jessie's -215 also supports this)
	if which systemd-escape >/dev/null 2>&1 ; then
		$(which systemd-escape) "$@"
		return $?
	fi

	# poor man's systemd escaping with perl
	if which perl >/dev/null 2>&1 ; then
		echo "$1" | perl -pe 'chomp; s%((?:^\.)|[^A-Za-z0-9:_./])%sprintf("\\x\%02x", ord($1))%ge; s%/%-%g; s%$%\n%;' 2>/dev/null
		return $?
	fi

	# very poor man's systemd escaping (no perl). Basic idea from:
	# http://it.toolbox.com/wiki/index.php/Uri_escape_bash_shell_function
	# Tested: bash, dash and mksh (2>/dev/null w/ printf supresses mksh warns)
	# Note that for mksh it only works in posix mode (i.e. run as /bin/sh,
	# because mksh assumes -e for echo by default) Also note that while this
	# escaping routine was tested in mksh, the rest of the script wasn't.
	_se_str="$1"
	_se_tmp="" 
	_se_first=1
	while [ -n "$_se_str" ] ; do
		case "$_se_str" in
		.*)
			# no leading dots allowed
			if [ $_se_first -eq 1 ] ; then
				_se_tmp="$(printf "%s\\\\x%02x" "$_se_tmp" "'$_se_str" 2>/dev/null)" #'
			else
				_se_tmp="$(printf "%s%c" "$_se_tmp" "$_se_str" 2>/dev/null)"
			fi
			;;
		/*)             _se_tmp="${_se_tmp}-" ;;
		[A-Za-z0-9:_]*) _se_tmp="$(printf "%s%c" "$_se_tmp" "$_se_str" 2>/dev/null)" ;;
		*)              _se_tmp="$(printf "%s\\\\x%02x" "$_se_tmp" "'$_se_str" 2>/dev/null)" ;; #'
		esac
		_se_str="${_se_str#?}"
		_se_first=0
	done
	echo "$_se_tmp"
}

# determine if a specific mount option is set, but it's not the only mount
# option in the list
has_mntopt_but_not_only()
{
	# Usage: has_mntopt option all_options
	case "$2" in
		"$1,"*|*",$1"|*",$1,"*) return 0 ;;
		*)                      return 1 ;;
	esac
}

# determine if a specific mount option is set, might be only one or one of many
has_mntopt()
{
	[ x"$1"x = x"$2"x ] && return 0
	has_mntopt_but_not_only "$1" "$2"
	return $?
}

# get own name
PROGNAME="$(basename "$0")"
if [ -z "$PROGNAME" ] ; then
	PROGNAME="bindmount-generator"
fi

# logging functions for /dev/kmsg (generate messages that look like syslog)
err() {
	echo "<3>${PROGNAME}[$$]: $@" >&2
	exit 1
}

warn() {
	echo "<4>${PROGNAME}[$$]: $@" >&2
}

notice() {
	echo "<5>${PROGNAME}[$$]: $@" >&2
}

info() {
	echo "<6>${PROGNAME}[$$]: $@" >&2
}

debug() {
	echo "<7>${PROGNAME}[$$]: $@" >&2
}

# make sure it's a file and readable
[ -f /etc/fstab ] || { info "/etc/fstab not a file" ; exit 0 ; }
[ -r /etc/fstab ] || { info "/etc/fstab not readable" ; exit 0 ; }

# unit dir: late generators
unit_dir="$3"

[ -d "$unit_dir" ] || err "generator dir $unit_dir not a directory"

# ALWAYS generate both units (makes it easier for the administrator if they
# want to override something, because they are always there, regardless of
# whether they are needed or not)
generate_unit local-fs.target > "$unit_dir/bindmount-local@.service"
generate_unit remote-fs.target > "$unit_dir/bindmount-remote@.service"

# read /etc/fstab, but remove all comments and empty lines first
sed 's%#.*%%' < /etc/fstab 2>/dev/null \
	| grep -v ^$ 2>/dev/null \
	| while read src dst fstype options dump pass _otherfields
do
	# Ignore lines we don't understand, i.e. either some fields are
	# completely empty, OR we parsed too many fields.
	if [ -z "$src" ] || [ -z "$dst" ] || [ -z "$fstype" ] || \
	[ -z "$options" ] || [ -z "$dump" ] || [ -z "$pass" ] || \
	[ -n "$_otherfields" ] ; then
		continue
	fi

	# remove duplicate slashes (same normalization that systemd does)
	dst="$(path_kill_slashes "$dst")"

	# we don't want to go anywhere NEAR the rootfs
	if [ x"$dst"x = x"/"x ] ; then
		continue
	fi

	# Note that doing additional sanity checks here, such as whether the mount
	# point exists or not, DOESN'T make sense, since we are run really early at
	# boot before anything itself is actually mounted, therefore checking for
	# this won't work in general (it might in very specific cases). Also, note
	# that systemd creates mount points by default if they don't exist yet, so
	# that's not an error anyway.

	# We don't support rbind (recursive bind mounts), because there it's not
	# clear where to apply the options to, so only check for the bind option.
	# Also, if it's only 'bind', then the user doesn't want to change options,
	# so we don't want to generate anything either (if /etc/fstab is changed
	# and systemd is to notice these changes, systemctl daemon-reload has to be
	# run and this generator is rerun as well).
	if has_mntopt_but_not_only "bind" "$options" ; then
		# remove slash at beginning, escape the value for usage in unit names
		dst=$(sdescape "${dst#/}")
		mkdir -p "${unit_dir}/${dst}.mount.d" 2>/dev/null
		if [ $? -ne 0 ] ; then
			warn "could not create ${unit_dir}/${dst}.mount.d"
			continue
		fi

		# Determine if this is on a remote filesystem, so we can decide our
		# Before= ordering properly. We use the same logic as systemd's
		# internal source code for this: a mount is considered to be remote
		# iff
		#	- the _netdev option is set
		#	OR
		#	- the filesystem type is a remote filesystem (see
		# is_network_filesystem above for details)
		if has_mntopt "_netdev" "$options" || \
		is_network_filesystem "$fstype" ; then
			UNIT="bindmount-remote"
		else
			UNIT="bindmount-local"
		fi

		# Using a drop-in is easiest, we don't want to check whether our unit
		# has been overridden (needed to use a symlink in .requires/)
		echo "[Unit]"                          >  "${unit_dir}/${dst}.mount.d/bindmount.conf"
		echo "Requires=${UNIT}@${dst}.service" >> "${unit_dir}/${dst}.mount.d/bindmount.conf"
	fi
done

Drop that file into /lib/systemd/system-generators as bindmount-generator.

Make the file executable:
sudo chmod +x /lib/systemd/system-generators/bindmount-generator

Set up your /etc/fstab file by putting the following at the end of the file:

# Set up mount-bind of Zorin OS-provided directories to point to directories on an external drive
# <file system>			<mount point>				<type>	<options>	<dump> <pass>
/home/$USER/Documents	/media/$USER/OldRB/Documents	none	rw,bind		0		0
/home/$USER/Music		/media/$USER/OldRB/Music		none	rw,bind		0		0
/home/$USER/Pictures		/media/$USER/OldRB/Pictures	none	rw,bind		0		0
/home/$USER/Videos		/media/$USER/OldRB/Videos	none	rw,bind		0		0

Issue the command:
sudo systemctl daemon-reload

After calling systemctl daemon-reload, one can see the effect of the bindmount-generator in the dependencies by issuing:
sudo systemctl show -p Before /media/$USER/OldRB/Documents
sudo systemctl show -p Before /media/$USER/OldRB/Music
sudo systemctl show -p Before /media/$USER/OldRB/Pictures
sudo systemctl show -p Before /media/$USER/OldRB/Video

NOTE: In a prior post, I promoted the use of noatime in fstab to turn off the updating of Access Time with each drive access. This increases data throughput, because the drive doesn't need to update the access time each time any given file is accessed. But that only works on an entire drive or partition... it won't work on a bind-mount (across two drives), and if you set it for a bind-mount (across two drives), none of the fstab options you've set up for that bind-mount will be applied... the bind-mount will fail. This applies as well for relatime. To check if an fstab option would work, try to set it manually first:
sudo mount -o remount,bind,{OPTION} /mountpoint

For instance:
sudo mount -o remount,bind,atime /media/$USER/OldRB/Documents
... if that works, it should also work in fstab when setting up a bind-mount.

The bind doesn't persist or the icons?

The bind doesn't persist. I haven't checked whether the icons persist... I'm still working on setting up /etc/fstab. I'm about to reboot... I'll report back.

[EDIT]
Ohhh, it's much more complicated than I'd first surmised. Systemd parallelizes the loading of drives, so I've got to figure out which dependencies are needed so the mount-bind actually works.

I'm off to sleep, I'll continue tomorrow.

I wonder though why going through this at all, and not just create a backup of those folders onto the external device periodically? You can even setup Synchthing to synchronize those folders (using one device is more involved but it's possible), if you need those two storage devices synchronized in real time.

Well, because:

  1. I want to be able to save to the default /home/$USER/Documents, and have it automatically save to the external drive, and...

  2. One doesn't learn if one doesn't try.

#1 is in keeping with my contention that no personal files should be saved on the same drive as the OS drive... if the OS crashes irreparably, you lose your personal files if they're on the same drive as the OS. Conversely, if they're saved to an external drive, you don't lose them even if you completely wipe and reinstall the OS, and should you need to bug out due to some catastrophe, you can just grab that external drive and go. Your files (being on an NTFS partition) are readable from any computer (Mac, Windows or Linux).

This one, though, is a big bite, almost too big for me... it involves setting up a systemd generator, inputting specialized entries into fstab with dependencies, etc.

I'm slowly figuring it out. Once I get it figured out, I'll post the entire procedure.

1 Like

I respect the effort here, but for me a simple ln -s did the trick, no fstab involved.

But, I am curious the details of your fstab mount and the actual error output if you don't mind sharing that is @Mr_Magoo

1 Like

I tried that. That puts a shortcut (soft sym-link) in the /home/$USER/Documents directory, but a user can still slip up and save personal files to /home/$USER/Documents.

This method ensures that even if you save to /home/$USER/Documents, that file goes to the external drive.

shouldn't your fstab be as simple as
/media/$USER/drive/Documents /home/$USER/Documents none bind 0 0
?

I feel like I'm missing something about this, if the drive is automounted in fstab this shouldn't this be the way?

No, systemd messes it up. See the addendum to my original post.

They had the same issue under sysvinit, but they could get around that by mounting twice so the mount options were honored... but systemd only allows mounting a directory once... so another work-around is needed.

I use deja dup for now... :sweat_smile:

1 Like