Now that we have a decent script that i would regard as 'safe enough' we can finally start to automate the whole process and have some fun. This is however also a good moment to set a goal and prevent getting lost in endless ideas and possibilities of coding.
As I discovered the possibilities of the Linux shell it seemed ever more logical to code a watchdog in shell script too. It really only needs to start the main program whenever the drive is plugged in, simple enough isn’t it?
While my plan initially was to use crondaemon as a watchdog I noticed I wasn’t using cron at all, so if someone else would use this the same might be the case. I don’t like adding unnecessary dependencies and while reading into the cron manual it dawned on me that it will not be the perfect tool for my idea, since a watchdog (I feel) needs to be running all the time in the background and cron only executes something at a certain interval.
Finding Nemo
I decided to leave the cron case open for now and focus on the next problem instes: "How to recognize the drive and having it mount automatically?" This can be challenging since Linux by default does not allow a normal user to mount a drive.
After a little googling I found that udisksctl from the udisks2 package is able to do this from the command line, IF (of course) you are member of the ‘storage’ group. I found various ways to look up a drive plugged in, but since we are going to use udiskctl why not use it to look up the drive too? This line seemed simple enough to me:
udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL"
It outputs a number (amount of times the variable is found), we're only interested if the value is 1
, because then we can do something like this:
echo "Looking for sync drive"
while true
do
case $(udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL") in
1 ) break
;;
esac
sleep 10
done
This creates an endless loop until the we get 1
as result, some don’t like coding like this, but I haven’t found issues with such lines in the past. Just remember to let the script pause (sleep
) for a while before trying again, otherwise it might impact the systems performance. Once the loop outputs 1
and breaks free, we might want to ask the user if he/she wants to start the sync job, maybe the drive is also used for other purposes. Perhaps this might also be something the user can setup in the configuration, but lets stick to the goal for now and keep it simple.
Graphical user interaction
We’ll assume that most users will run a graphical environment, thus we need to interact with that. This is done with a tool called zenity, like so:
zenity --question --text="Sync drive detected. Would you like to sync now?"
Wow, that was too easy. There are lot more options to zenity to play around with, but this does the job nicely and presents a pretty graphical yes or no dialog to the user.
Mounting the drive
In case we got the green light from the user we now need to know if the drive is perhaps already mounted or if we still need to do that.
[[ -z $(mount | grep "$DEVICE") ]] && udisksctl mount -b "$DEVICE" ||
The DEVICE variable needs to be enumerated beforehand, naturally. This might look a little messy:
DEVICE=$(udisksctl dump | grep -i "IdLabel: \+$DISK_LABEL" -B 12 | grep " Device:" | cut -d ":" -f 2 | sed 's/^[ \t]*//')
Now with that we could write the script, but what if the user did not want to sync or the sync job finished, that would restart the the watchdog and annoy the user with a dialog again. At the primary side this might make sense (since new data could be generated at any moment), but that’s not a likely scenario and the user can always run the main program him/herself when there is a need to. In short, i feel the user should not be bothered until the drive has been disconnected (and connected again).
To detect wether the drive is disconnected or not we simply run the our previous code again but now we need 0
instead of 1
:
while true
do
case $(udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL") in
0 ) break
;;
esac
sleep 1h
done
Since there is probably not much hurry after a sync or dialog, I increased the sleep time.
Options & Optimizations
Now we have enough to make a complete script but when I started copy variables from the main script it all felt somewhat inefficient and unorganized. This was a good time to consider a config file and would have to be done later anyway if it was to become a neat package.
I thought loading a file was complicated but it turned out this was ridiculously simple:
. /home/$USER/.config/staffetta/staffetta.conf
:-D
Ok, time for some coding. This is what I came up with:
#!/bin/bash
# staffetta-autosync.sh
VERSION=0.2.1
# Author: Jochum Döring, jochum (dot) doring (at) gmail (dot) com
# License: CC BY-SA
# About: This script will find the configured sync disk and start staffetta
# Usage: Setup configuration parameters and run 'staffetta --enable-autosync' to enable autosync
# URL: https://sites.google.com/site/joochdoesnotcompute/software/syncing-servers-offline
if [ -r /home/$USER/.config/staffetta/staffetta.conf ]; then
. /home/$USER/.config/staffetta/staffetta.conf
else
yes_or_no "$msg Configuration not found, copy default config?" &&
cp /etc/staffetta/default.conf /home/$USER/.config/staffetta/staffetta.conf
if [ -r /usr/bin/nano ]; then
nano /home/$USER/.config/staffetta/staffetta.conf
else
echo "Please setup your config and restart:
/home/$USER/.config/staffetta/staffetta.conf"
exit 1
fi
fi
while true
do
echo "Looking for sync drive"
while true
do
case $(udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL") in
1 ) break
;;
esac
sleep $AUTOSYNC_SCAN
done
if zenity --width=250 --title=Staffetta --question --text="Sync drive detected. Would you like to sync now?"; then
DEVICE=$(udisksctl dump | grep -i "IdLabel: \+$DISK_LABEL" -B 12 | grep " Device:" | cut -d ":" -f 2 | sed 's/^[ \t]*//')
[[ -z $(mount | grep "$DEVICE") ]] && udisksctl mount -b "$DEVICE" || false
xterm -e "staffetta -z"
echo "sync finished, waiting for sync drive to disconnect"
while true
do
case $(udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL") in
0 ) break
;;
esac
sleep 1h
done
else
echo "User aborted sync, waiting for sync drive to disconnect"
while true
do
case $(udisksctl dump | grep IdLabel | grep -c -i "$DISK_LABEL") in
0 ) break
;;
esac
sleep 1h
done
fi
done
With the watchdog done I needed a way to easily enable or disable it, as i imagine not everyone needs or wants to use it. It would be nice to have an option/switch in the main script and this also turned out easier than I thought. simply with:
[[ $1 = "--help" ]] && print_help ||
$1
grabs any input added after the given command. Nice, this made a lot of other ideas I had for the main program a lot easier. That’s where I let myself run free for a while. I also included an option to disable unnecessary questions in the main program that would not matter being run by the watchdog. The following options were added:
-h, --help | Display help / options |
-r | Rebuild sync base (only possible at secondary location) |
-z | Only ask user what to do in case of an error |
--enable-autosync | Enable the autosync function (copy the supplied desktop file to the users autostart folder) |
--disable-autosync | Disable the autosync function (remove the desktop file from the users autostart folder) |
With all the new options it became necessary to improve code and go to a certain line in the script, but this is not directly possible in shell, so I put everything in functions in effect making the script more efficient.
The actual main script (without the functions) now looks like this:
if [ -r /home/$USER/.config/staffetta/staffetta.conf ]; then
. /home/$USER/.config/staffetta/staffetta.conf
else
yes_or_no "$msg Configuration not found, copy default config?" &&
mkdir /home/$USER/.config/staffetta/
cp /etc/staffetta/default.conf /home/$USER/.config/staffetta/staffetta.conf
if [ -r /usr/bin/nano ]; then
nano /home/$USER/.config/staffetta/staffetta.conf
. /home/$USER/.config/staffetta/staffetta.conf
else
echo "Please setup your config and restart:
/home/$USER/.config/staffetta/staffetta.conf"
exit 1
fi
fi
[[ $1 = "-h" ]] && print_help ||
[[ $1 = "--help" ]] && print_help ||
[[ $1 = "--enable-autosync" ]] && enable_autosync ||
[[ $1 = "--disable-autosync" ]] && disable_autosync ||
[[ $1 = "-r" ]] && build_sync_base ||
if [[ $1 = "-z" ]]; then
export noconfirm="1"
fi
if [ -d "$MOUNT_ROOT/$DISK_LABEL/$SYNCDIR/" ]; then
if [[ find_token -gt 1 ]]; then
yes_or_no "$msg ERROR: Multiple tokens found! Remove and configure token?" &&
echo "Removing all tokens from /home/$USER/"
rm /home/$USER/.config/staffetta/staffetta-token.pri
rm /home/$USER/.config/staffetta/staffetta-token.sec
configure_token
main_program
else
main_program
fi
else
find_drive_and_mount
[ ! -d "$MOUNT_ROOT/$DISK_LABEL/$SYNCDIR/" ] && build_sync_base || true
main_program
fi
As you can see, this now makes use of the same config file as the watchdog does. I put the other variables within each function to speed up the script, some of these variables enumerate stuff which can considerably slow things down. Quite annoying if you only want to read the help ;-)
Since the code was becoming a lot cleaner I also felt the need to improve the way staffetta writes the states. This used to be done with these files:
.staffetta-state.{ready/lock}
.staffetta-last.{pri/sec}
.staffetta-base-date.{datestamp}
.staffetta-base-build-date.{datestamp}
.staffetta-empty_files.{datestamp}.{amount}
Now all the variables are inside one file: staffetta.state, and loaded the same way as I did with the config file. This also reduces hard drive writes, which is a good thing on external drives, since they usually have slower access times.
Packaging
With the autosync script done it was time to make a package, as without one it could get messy with versioning. I work almost exclusively on Arch Linux based distro's, so at this point in development only an Arch based packages will be released. But if you are impatient, feel free to grab a copy of the source below.
The PKGBUILD is not very interesting, just some things i took from the packages i maintain and some stuff i took from examples in the wiki:
# Submitter: jooch <jochum.doring (at) gmail (dot) com>
# Maintainer: jooch <jochum.doring (at) gmail (dot) com>
pkgname=staffetta
pkgver=0.3.3
pkgrel=2
pkgdesc="Offline sync tool using rsync and a USB drive as medium"
arch=('i686' 'x86_64')
url="https://sites.google.com/site/joochdoesnotcompute/software/syncing-servers-offline"
license=('GPL3')
depends=('rsync' 'xterm' 'zenity' 'gawk' 'udisks2' 'grep' 'sed')
provides=("$pkgname")
source=("default.conf"
"default.state"
"icon24.png"
"staffetta.desktop"
"staffetta.sh"
"staffetta-autosync.sh"
"staffetta-autosync.desktop")
sha256sums=('b9d4de4ae2418ba41270b237acd7c2be8b13b1c005baa5ba31b14d93c720b769'
'239aaf2d557f89724adeda84b7d8c096391447f88e0138ab5fc6637b10bbea69'
'762fe186c8115e28dc5c9d5505e210484471ae9bbadf4a7732022553f4682192'
'e7325d57bc7d878195aa18bfb597a4ca77a04326098104e7ce7a14748d13f77e'
'33e0d71bfb9f53dc8ecbd53be04596c02449c9145fc60a70f979594fdd9476b1'
'0ef22353f3a4be6191da38a5f8d507bc23a2e2e3677082263d7b1c558debf0a7'
'3e73e0b6f30e6731d34d7c35dd1fcd152f58022a069f404850d0ac347cba0246')
package() {
install -dm755 "$pkgdir/usr/bin"
install -m755 "$srcdir/staffetta.sh" "$pkgdir/usr/bin/staffetta"
install -m755 "$srcdir/staffetta-autosync.sh" "$pkgdir/usr/bin/staffetta-autosync"
install -d "$pkgdir/usr/share/applications"
install -m644 "$srcdir/staffetta.desktop" "$pkgdir/usr/share/applications/staffetta.desktop"
install -d "$pkgdir/etc/staffetta"
install -m644 "$srcdir/staffetta-autosync.desktop" "$pkgdir/etc/staffetta/staffetta-autosync.desktop"
install -d "$pkgdir/etc/staffetta"
install -m644 "$srcdir/default.conf" "$pkgdir/etc/staffetta/default.conf"
install -d "$pkgdir/etc/staffetta"
install -m644 "$srcdir/default.state" "$pkgdir/etc/staffetta/default.state"
local icon_size icon_dir
for icon_size in 24; do
icon_dir="$pkgdir/usr/share/icons/hicolor/${icon_size}x${icon_size}/apps"
install -d "$icon_dir"
install -m644 "$srcdir/icon${icon_size}.png" "$icon_dir/staffetta.png"
done
}
Reviewing the work
I've been using the program now for some weeks and it works nice. There where a few hiccups in the code, but not much worth writing about, most were related to sleep deprivation :-).
I quite enjoy the fact that my files are now so up te date which really improves my productivity. My test case however is by any means very tiny at the moment, only two systems involved and one external USB drive. So i would gladly hear if anyone has suggestions or bugs to share, feel free to write.
Already i'm thinking about what i would wish to have in the next version, would be great to have multiple directories and secondary locations!