simple_backup/simple-backup
2017-03-11 21:04:05 +01:00

422 lines
12 KiB
Bash
Executable File

#!/bin/bash
#Copyright 2015 Daniele Fucini <dfucini@gmail.com>
#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program. If not, see <http://www.gnu.org/licenses/>.
#Simple backup script. Reads options, sources and destination from a configuration file or standard input
#Help function
function help_function {
echo "simple_backup, version 1.3"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo "-h, --help Print this help and exit."
echo "-c, --config CONFIG_FILE Use the specified configuration file"
echo " instead of the default one."
echo " All other options are ignored."
echo "-i, --input INPUT [INPUT...] Specify a file/dir to include in the backup."
echo "-d, --directory DIR Specify the output directory for the backup."
echo "-e, --exclude PATTERN [PATTERN...] Specify a file/dir/pattern to exclude from"
echo " the backup."
echo "-k, --keep NUMBER Specify the number of old backups to keep."
echo " Default: keep all."
echo "-u, --user USER User performing the backup."
echo " Default: current user."
echo " WARNING: This currently doesn't work with"
echo " other options!"
echo ""
echo "If no option is given, the program uses the default"
echo "configuration file: $HOMEDIR/.simple_backup/config."
echo ""
echo "Report bugs to dfucini@gmail.com"
exit 0
}
#Read configuration file
function read_conf {
if [[ "$#" -eq 0 ]]; then
CONFIG="$HOMEDIR/.simple_backup/config"
if [[ ! -f "$CONFIG" ]]; then
#If default config file doesn't exist, exit
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: Configuration file not found" | tee $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
exit 1
fi
else
CONFIG="$1"
if [[ ! -f "$CONFIG" ]]; then
#If the provided configuration file doesn't exist, exit
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: Configuration file not found" | tee -a $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
exit 1
fi
fi
#Create temporary files
INPUTS=$(mktemp)
EXCLUDE=$(mktemp)
BACKUP=$(mktemp)
NKEEP=$(mktemp)
#Parse the configuration file
awk -v INPUTS="$INPUTS" -v EXCLUDE="$EXCLUDE" -v BACKUP="$BACKUP" \
-v NKEEP="$NKEEP" -v UHOME="$HOMEDIR/" -F '[=,]' \
'$1=="inputs" { for ( i=2; i<=NF; i++ ) { sub(/^~\//,UHOME,$i); print $i > INPUTS } }
$1=="backup_dir" { sub(/^~\//,UHOME,$2); print $2 > BACKUP }
$1=="exclude" { for ( i=2; i<=NF; i++ ) { sub(/^~\//,UHOME,$i); print $i > EXCLUDE } }
$1=="keep" { if ( $2 != NULL ) { print $2 > NKEEP } }' $CONFIG
BACKUP_DEV=$(cat $BACKUP)
KEEP=$(cat $NKEEP)
rm "$BACKUP"
rm "$NKEEP"
if [[ -z "$BACKUP_DEV" || ! -d "$BACKUP_DEV" ]]; then
#If the backup directory is not set or doesn't exist, exit
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: Output folder \"$BACKUP_DEV\" not found" | tee -a $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
rm "$INPUTS"
rm "$EXCLUDE"
exit 1
fi
BACKUP_DIR=$BACKUP_DEV/simple_backup
DATE=$(date +%Y-%m-%d-%H:%M)
#Create the backup subdirectory using date
if [[ ! -d "$BACKUP_DIR" ]]; then
mkdir -p "$BACKUP_DIR/$DATE"
else
#If previous backup(s) exist(s), save link to the last backup
LAST_BACKUP=$(readlink -f "$BACKUP_DIR/last_backup")
mkdir "$BACKUP_DIR/$DATE"
fi
#Set the backup directory variable to the newly created subfolder
BACKUP_DIR="$BACKUP_DIR/$DATE"
n_in=$(cat $INPUTS | wc -l)
return
}
#Parse options
function parse_options {
i=1
n_in=0
#Create a temporary file to store inputs
INPUTS=$(mktemp)
#Create temp file to store exclude patterns
EXCLUDE=$(mktemp)
while [[ "$#" -gt 0 ]]
do
var="$1"
case "$var" in
-h | --help)
help_function
rm $INPUTS
rm $EXCLUDE
exit 0
;;
-i | --input)
while [[ "$#" -gt 1 && ! "$2" =~ ^- ]]
do
input="$2"
if [[ -z "$input" ]]; then
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: bad options format" | tee -a $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
rm $INPUTS
rm $EXCLUDE
exit 1
fi
if [[ ! -e "$input" ]]; then
echo "Warning: input \"${INPUTS[$i]}\" not found. Skipping" | tee -a $WARN
else
i=$((i+1))
n_in=$((n_in+1))
echo "$input" >> "$INPUTS"
fi
shift
done
;;
-d | --directory)
BACKUP_DEV="$2"
if [[ -z "$BACKUP_DEV" || ! -d "$BACKUP_DEV" ]]; then
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: output folder \"$BACKUP_DEV\" not found" | tee -a $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
rm $INPUTS
rm $EXCLUDES
exit 1
fi
BACKUP_DIR="$BACKUP_DEV/simple_backup"
DATE=$(date +%Y-%m-%d-%H:%M)
#Create the backup subdirectory using date
if [[ ! -d "$BACKUP_DIR" ]]; then
mkdir -p "$BACKUP_DIR/$DATE"
else
#If previous backup(s) exist(s), save link to the last backup
LAST_BACKUP=$(readlink -f "$BACKUP_DIR/last_backup")
mkdir "$BACKUP_DIR/$DATE"
fi
#Set the backup directory variable to the newly created subfolder
BACKUP_DIR="$BACKUP_DIR/$DATE"
shift
;;
-e | --exclude)
while [[ "$#" -gt 1 && ! "$2" =~ ^- ]]
do
echo "$2" >> "$EXCLUDE"
shift
done
;;
-k | --keep)
KEEP="$2"
shift
;;
-c | --config)
rm "$EXCLUDE"
rm "$INPUTS"
read_conf "$2"
return
;;
-u | --user)
rm "$EXCLUDE"
rm "$INPUTS"
if [[ ! -d "/home/$2" ]]; then
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: user $2 doesn't exist" | tee -a $ERR
mv $LOG "HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "HOMEDIR/.simple_bakup/errors.log"
mv $WARN "HOMEDIR/.simple_backup/warnings.log"
exit 1
fi
if [[ ! -d "/home/$2/.simple_backup" ]]; then
mkdir "/home/$2/.simple_backup"
echo "Created directory \"$HOMEDIR/.simple_backup\"."
echo "Copy there sample configuration and edit it"
echo "to your needs before running the backup."
exit 1
fi
HOMEDIR="/home/$2"
config="/home/$2/.simple_backup/config"
USER="$2"
read_conf "$config"
return
;;
*)
echo "$(date): Backup failed (see errors.log)" >> $LOG
echo "Backup failed"
echo "Error: Option $1 not recognised. Use 'simple-backup -h' to see available options" | tee -a $ERR
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
exit 1
;;
esac
shift
done
return
}
#Create temporary log files
LOG=$(mktemp)
ERR=$(mktemp)
WARN=$(mktemp)
HOMEDIR="$HOME"
#Check number of parameters
if [[ "$#" -eq 0 ]]; then
if [[ ! -d "$HOMEDIR/.simple_backup" ]]; then
mkdir "$HOMEDIR/.simple_backup"
echo "Created directory \"$HOMEDIR/.simple_backup\"."
echo "Copy there sample configuration and edit it"
echo "to your needs before running the backup."
exit 1
fi
read_conf
else
parse_options $@
fi
if [[ -z $n_in || $n_in -eq 0 ]]; then
echo "$(date): Backup finished (no files copied)" >> $LOG
echo "Backup finished"
echo "Warning: no valid input selected. Nothing to do" | tee -a $WARN
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
exit 0
fi
echo "$(date): Starting backup" > $LOG
echo "Starting backup..."
#If specified, keep the last n backups and remove the others. Default: keep all
if [[ -n $KEEP ]]; then
N_BACKUP=$(ls -l $BACKUP_DEV/simple_backup | grep -c ^d)
N_BACKUP=$(($N_BACKUP-1))
if [[ $N_BACKUP -gt $KEEP ]]; then
echo "$(date): Removing old backups" >> $LOG
echo "Removing old backups..."
REMOVE=$(mktemp)
find $BACKUP_DEV/simple_backup/* -maxdepth 0 -type d | sort | head -n $(($N_BACKUP - $KEEP)) >> $REMOVE
while read line
do
rm -r $line
echo "Removed backup: $line" >> $LOG
done<$REMOVE
echo "Removed old backups"
rm $REMOVE
fi
fi
# Sort input files for rsync efficiency
if [[ ! -z "$INPUTS" ]]; then
sort "$INPUTS" -o "$INPUTS"
fi
if [[ -z "$LAST_BACKUP" ]]; then
rsync -acrv -H -X -R --exclude-from="$EXCLUDE" --files-from="$INPUTS" / "$BACKUP_DIR" --ignore-missing-args >> $LOG 2>> $ERR
else
rsync -acrv -H -X -R --link-dest="$LAST_BACKUP" --exclude-from="$EXCLUDE" --files-from="$INPUTS" / "$BACKUP_DIR" --ignore-missing-args >> $LOG 2>> $ERR
fi
#Update the logs
if [[ $(cat $ERR | wc -l) -gt 0 ]]; then
echo "$(date): Backup finished with errors" >> $LOG
echo "Backup finished with errors"
elif [[ $(cat $WARN | wc -l) -gt 0 ]]; then
echo "$(date): Backup finished with warnings" >> $LOG
echo "Backup finished (warnings)"
else
echo "$(date): Backup finished" >> $LOG
echo "Backup finished"
fi
#Fix ownership and permissions of log files if needed
if [[ ! -z $USER ]]; then
chown $USER:$USER $LOG && chmod 644 $LOG
chown $USER:$USER $ERR && chmod 644 $ERR
chown $USER:$USER $WARN && chmod 644 $WARN
fi
#Copy log files in home directory
mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log"
mv $ERR "$HOMEDIR/.simple_backup/errors.log"
mv $WARN "$HOMEDIR/.simple_backup/warnings.log"
if [[ ! -z "$EXCLUDE" ]]; then
rm "$EXCLUDE"
fi
if [[ ! -z "$INPUTS" ]]; then
rm "$INPUTS"
fi
if [[ -L "$BACKUP_DEV/simple_backup/last_backup" ]]; then
rm "$BACKUP_DEV/simple_backup/last_backup"
fi
BACKUP_DIR_FULL=$(readlink -f "$BACKUP_DIR")
ln -sf "$BACKUP_DIR_FULL" "$BACKUP_DEV/simple_backup/last_backup"
exit 0