Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: e9c6e341f3
Fetching contributors…

Cannot retrieve contributors at this time

executable file 466 lines (370 sloc) 13.2 kB
#!/bin/bash
VER="1.0"
CONFIG="/etc/squashfu.conf"
source "$CONFIG"
# Informational output w/ happy colors
debug () {
if [[ "$DEBUG" == "true" ]]; then
printf '\033[1;33mDEBUG ::\033[1;m %s\n' "$*"
fi
}
warn () {
printf '\033[1;33m::\033[1;m %s\n' "$*"
}
info () {
printf '\033[1;34m::\033[1;m %s\n' "$*"
}
die () {
printf '\033[1;31m::\033[1;m %s\n' "$*" >&2
exit 1
}
create_new_squash () {
# Args: number of bins to be squashed, -1 on initial creation
# Returns: 0 on success, non-zero on failure
# If making first seed, create it directly from source
if [[ $1 -eq -1 ]]; then
info "Creating seed from sources (this may take a while)"
mksquashfs "${INCLUDES[@]}" "$SEED" -b 65536 -e "${EXCLUDES[@]}" >/dev/null
if [[ $? -eq 0 ]]; then
mount_squash
info "Seed creation finished. It has been mounted at "$SQUASH_MOUNT" if you would like to make sure the proper files are included"
return 0;
else
die "There was an error creating the initial squash."
fi
fi
# Determine oldest $1 bins and mount them with the current squash
local old_bins=($(sort -n -r -t: -k2 "$BINVENTORY" | tail -$1 | cut -d: -f1))
mount_union_with_bins ${old_bins[@]}
info "Merging old incrementals"
# Create new squash with temp name
mksquashfs "$UNION_MOUNT" "$SEED.replace" -b 65536 >/dev/null
# If the squash wasn't made correctly, we don't want to continue
if [[ $? -ne 0 ]]; then
return 1
fi
unmount_all
# Replace old squash
mv "${SEED}.replace" "$SEED"
info "Cleaning up inventory"
# Delete old bins, and remove entry from binventory
for bin in ${old_bins[@]}; do
rm -rf "${BINS_DIR}/$bin"
sed -i "/^$bin:/d" "$BINVENTORY"
done
# Clean up $binventory
sweep_bins
}
create_new_bin () {
# Arguments: 1, the number of the bin to create
# Returns: 0 on success, non-zero on error
debug "Asked to create new bin: $1"
# Create new directory, fail if it exists (something's wrong)
mkdir "${BINS_DIR}/$1"
if [[ $? -ne 0 ]]; then
return $?
fi
# Update binventory with new bin name and timestamp
echo "${1}:$(date +%s)" >> "$BINVENTORY"
# If write to bin list fails, remove diretory and exit
if [[ $? -ne 0 ]]; then
rmdir "${BINS_DIR}/${1}"
die "Error writing to '$BINVENTORY'"
fi
return
}
# Mounting functions
mount_squash () {
# Arguments: none
# Returns: return code of mount command
debug "Mounting Squash"
mount -o loop,ro "$SEED" "$SQUASH_MOUNT"
return $?
}
mount_union_with_bins () {
# Arguments: numbers of bins to be mounted (variable number)
# Returns: 0 on successful mount, non-zero on failure
info "Mounting union"
debug "Requested to mount bins: $*"
# Mount first as rw, shift, and mount the rest ro
branches="br=${BINS_DIR}/$1=rw:"; shift
if [[ -n $1 ]]; then
for bin in $*; do
branches="${branches}${BINS_DIR}/$bin=ro:"
done
fi
branches="${branches}${SQUASH_MOUNT}=ro"
mount -t aufs none "$UNION_MOUNT" -o udba=reval,$branches
return $?
}
# Unmounting functions
unmount_union () {
# Args: none
# Returns: return code from umount
info "Unmounting union"
$(mountpoint -q "$UNION_MOUNT") && umount "$UNION_MOUNT" 2>/dev/null
return $?
}
unmount_squash () {
# Args: none
# Returns: return code from umount
info "Unmounting squash"
$(mountpoint -q "$SQUASH_MOUNT") && umount "$SQUASH_MOUNT" 2>/dev/null
return $?
}
unmount_all () {
# Args: none
# Returns: none
if [[ $UID -ne 0 ]]; then
die "Must be root to unmount."
fi
# Union MUST be unmounted first
unmount_union && unmount_squash || die "Failure during unmounting"
}
check_for_resquash () {
# Args: none
# Returns: number of bins needing to be merged
local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l)
debug "Found $number_of_bins bins"
if [[ $number_of_bins -gt $MAX_BINS ]]; then
return $(( $number_of_bins - $MIN_BINS ))
else
return 0
fi
}
get_next_available_bin () {
# Arguments: none
# Returns: Numeric value of the next unused bin
next_bin=$(( $(cut -d: -f1 "$BINVENTORY" | sort -n | tail -1) + 1 ))
debug "Next available bin = $next_bin"
return $next_bin
}
sweep_bins () {
# Arguments: none
# Returns: none
info "Rotating chickens"
# Make sure bins are numbered in order, clean up if not. In other words,
# if we have 10 bins, make sure they're ordered 1 through 10.
count=1
ls "${BINS_DIR}" | while read bin; do
if [[ ! -d "${BINS_DIR}/$count" ]]; then
high_bin=$(ls "${BINS_DIR}" | sort -n | tail -1)
debug "Sweeping bin $high_bin into bin $count"
mv "${BINS_DIR}/$high_bin" "${BINS_DIR}/$count"
sed -i "/^$high_bin:/s/^$high_bin:/$count:/" "$BINVENTORY"
fi
count=$(( $count + 1 ))
done
}
action_backup () {
# Args: options array squashfu was invoked with, shifted 1
# Returns: none
if [[ $UID -ne 0 ]]; then
die "Must be root to perform a backup"
fi
# Does the binventory exist? If not, prompt to make sure this is an initialization
if [[ ! -f "$BINVENTORY" || ! -f "$SEED" ]]; then
read -p "Looks like this is your first time running SquashFu. Is this correct? (y/n) " ans
while [[ true ]]; do
case $ans in
[yY]) break ;;
[nN]) die "Your bin inventory and/or seed seem to be missing. Please fix this before continuing." ;;
*) ;;
esac
done
# If we got here, the user answered yes, so initialize a new structure
mkdir -p "$UNION_MOUNT"
mkdir -p "$SQUASH_MOUNT"
mkdir -p "${BINS_DIR}"
touch "$BINVENTORY"
create_new_squash -1
exit 0
fi
info "Backup requested at $(date --rfc-3339=seconds)"
# Cleanup union, in case user was doing a rollback and forgot to unmount (or error on last run)
mountpoint -q "$UNION_MOUNT" && unmount_union
info "Creating new bin"
# Make a new bin for this incremenetal
get_next_available_bin
local new_bin=$?
create_new_bin $new_bin
# Determine the mount order via binventory
local bin_order=($(sort -n -r -t: -k2 "$BINVENTORY" | cut -d: -f1))
mountpoint -q "${SQUASH_MOUNT}" || mount_squash
mount_union_with_bins ${bin_order[@]}
# Die with error on mount, else start rsync
if [[ $? -ne 0 ]]; then
return 1;
fi
# Includes are pulled in directly from config
EXCLUDES=$(for excl in ${EXCLUDES[@]}; do echo --exclude $excl; done)
debug "rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT""
info "Creating new incremental"
/usr/bin/rsync ${RSYNC_OPTS[@]} ${INCLUDES[@]} ${EXCLUDES[@]} "$UNION_MOUNT"
rsync_ret=$?
for error in ${DEL_BIN_ON_FAIL[@]}; do
if [[ $rsync_ret == $error ]]; then
warn "Unexpected hangup by rsync ($error). Deleting backup."
action_remove_bin $new_bin override
fi
done
check_for_resquash
if [[ val=$? -gt 0 ]]; then
create_new_squash $val
fi
# TODO: Report if requested
unmount_all
info "Backup completed at $(date --rfc-3339=seconds)"
}
action_remove_bin () {
# check if the bin exists both in the binventory AND in the bins directory
if [[ $UID -ne 0 ]]; then
die "Must be root to remove a backup"
fi
if [[ ! -w "$BINVENTORY" ]]; then
die "Error writing to ${BINVENTORY}"
fi
if [[ $(grep -E "^$1:" ${BINVENTORY}) && -d "${BINS_DIR}/$1" ]]; then
if [[ -z $2 ]]; then
echo "Are you SURE you want to remove this bin?"
local timestamp=$(sed -n "/^$1:/s/^[0-9]*:\([0-9]*\)/\1/p" "${BINVENTORY}")
printf "\t%15s %s\n\t%15s %s\n\t%15s %s\n" \
"Bin:" "$1" \
"Date Created:" "$(date --rfc-3339=seconds --date="@$timestamp")" \
"Size:" "$(du -sh "${BINS_DIR}/$1" 2>/dev/null | awk '{print $1}')"
read -p "Confirm deletion (y/N)" confirm
if [[ $confirm != "y" ]]; then
info "Delete operation aborted"
exit 1
fi
fi
info "Deleting bin $1"
sed -i "/^$1:[0-9]*/d" "${BINVENTORY}"
rm -rf ${BINS_DIR}/$1
# tidy up!
sweep_bins
else
die "Bin $1 not found."
fi
}
action_rollback () {
# Args: number of backups to roll back
# Returns: none
if [[ $UID -ne 0 ]]; then
die "Must be root to perform a rollback"
fi
# Validate input with test cases
if [[ -z $1 ]]; then
die "The rollback action requires 1 additional argument."
fi
# Form a chronologically ordered list of bins, assuming the user didn't give bogus input
local bin_list=($(grep -vE "^[ \t]*$" "$BINVENTORY" | sort -t: -r -n -k2 | cut -d: -f1))
if [[ $1 -gt ${#bin_list[@]} ]]; then
die "Cannot rollback more than ${#bin_list[@]} backups"
fi
local num_to_mount=$(( ${#bin_list[@]} - $1 ))
mountpoint -q "$UNION_MOUNT" && unmount_union
mountpoint -q "$SQUASH_MOUNT" || mount_squash
mount_union_with_bins ${bin_list[@]:(-$num_to_mount)}
local rb_timestamp=$(grep -E "^${bin_list[@]:(-$num_to_mount):1}:" "$BINVENTORY" | cut -d: -f2)
info "You have rolled back to $(date --rfc-3339=seconds --date="@$rb_timestamp")"
info "Your files can be found at '${UNION_MOUNT}'"
}
action_report () {
info "SquashFu Usage Report"
echo
# Enumerate bins, sort date order, print human readable create date and size
cd "$BINS_DIR"
OLDIFS=$IFS;IFS=${IFS}:
# Collect all data into an array to 'preload' it. Index 0 is the entire
# folder. The following indicies correspond to the bin number of that index
printf "%30s\r" " .: Loading :." >&2
DATA=($(du -sh . * 2>/dev/null | sort -n -k2 | awk '{print $1}'))
printf "%30s\r" " " >&2
printf "%10s\t%25s\t%7s\n" "Bin ID" "Date Created" "Size"
grep -vE "^[\t ]*$" "$BINVENTORY" | sort -r -t: -k2 -n | while read bin stamp; do
printf "%10d\t%25s\t%7s\n" "$bin" \
"$(date --rfc-3339=seconds --date="@$stamp")" \
"${DATA[$bin]}"
done
IFS=$OLDIFS
printf "%10s\t%25s\t%7s\n" "" "Incremental Total" "${DATA[0]}"
# Print totals (not efficient -- reruns du on things we already ran it on)
printf "\n%10s\t%25s\t%7s\n" "" "$(basename $SEED)" "$(du -h "${SEED}" | awk '{print $1}')"
printf "\n%10s\t%25s\t%7s\n" "" "Grand Total" \
"$(du -csh "$BINS_DIR" "$SEED" 2>/dev/null | grep -E "total$" | awk '{print $1}')"
}
action_resquash_now () {
# Args: none
# Returns: none
if [[ $UID -ne 0 ]]; then
die "Must be root to perform a resquash"
fi
info "Voluntary resquash requested"
local number_of_bins=$(grep -vE "^[ ]*$" "$BINVENTORY" | wc -l)
if [[ $number_of_bins -le $MIN_BINS ]]; then
die "Nothing to do. Current backups do not exceed MIN_BINS value."
else
create_new_squash $(( $number_of_bins - $MIN_BINS ))
fi
exit $?
}
action_unmount () {
unmount_all
}
usage () {
info "SquashFu: Super Awesome Backup Express (Professional Edition)"
echo "version: $VER"
cat <<HELP
USAGE
squashfu <action> [options]
ACTIONS
-B
Runs a regular backup, using the config file at /etc/squashfu.
-C
Create a new squash by merging old bins. This will still leave you
with $MIN_BINS backups (as per the MIN_BINS setting in your config).
-D <bin number>
Delete an incremental backup. This is done interactively and you will have
a chance to confirm before any files are deleted.
-Q
Displays the size of the seed, the incrementals, and totals.
-R <number of bins>
Rollback specified number of backups and mount union for browsing. The rolled
back data will be mounted at $UNION_MOUNT.
-U
Unmount squash and union. Although SquashFu will always check and unmount as
necessary before an operation, this is provided as a safeguard.
OPTIONS
-c <path_to_config>
Specify an alternate config file. Options defined will override options in
the default config. If you specify alternate locations via a supplmenetal config,
you will need to provide the config for all actions
HELP
exit 1
}
while getopts :BCD:QR:Uc: opt; do
case $opt in
B)
action=backup ;;
C)
action=resquash_now ;;
D)
action="remove_bin $OPTARG" ;;
Q)
action=report ;;
R)
action="rollback $OPTARG" ;;
U)
action=unmount ;;
c)
[[ -f $OPTARG ]] && source $OPTARG ;;
\:)
die "Argument missing from -$OPTARG"
usage ;;
\?)
die "Unrecognized option -$OPTARG"
usage ;;
esac >&2
done
[[ -n $action ]] && action_$action || usage
Jump to Line
Something went wrong with that request. Please try again.