#!/bin/bash #Copyright 2017 Daniele Fucini #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 . #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.4" 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 #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 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 #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 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 #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 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 backups exist, 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 { 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 [[ ! -e "$input" ]]; then echo "Warning: input \"$input\" not found. Skipping" | tee -a $WARN else 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 #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 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" 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) 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 if [[ ! -d "$HOMEDIR/.simple_backup" ]]; then mkdir "$HOMEDIR/.simple_backup" 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 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 if [[ ! -d "/home/$2/.simple_backup" ]]; then mkdir "/home/$2/.simple_backup" echo "Created directory \"$HOMEDIR/.simple_backup\"." fi HOMEDIR="/home/$2" USER="$2" shift ;; *) 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 #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 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" OPTIONS="-arvh -H -X -R" #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," echo "or pass options on the command line." exit 1 fi read_conf else parse_options $@ if [[ $n_in -gt 0 && ( -z $BACKUP_DIR || ! -d $BACKUP_DIR ) ]]; 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 #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 mv $LOG "$HOMEDIR/.simple_backup/simple_backup.log" mv $ERR "$HOMEDIR/.simple_backup/errors.log" mv $WARN "$HOMEDIR/.simple_backup/warnings.log" exit 1 elif [[ $n_in -eq 0 && -z $BACKUP_DIR ]]; 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," echo "or pass options on the command line." exit 1 fi read_conf $HOMEDIR/.simple_backup/config fi fi if [[ $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 #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 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 echo "Copying files. This may take a long time..." if [[ -z "$LAST_BACKUP" ]]; then rsync $OPTIONS --exclude-from="$EXCLUDE" --files-from="$INPUTS" / "$BACKUP_DIR" --ignore-missing-args >> $LOG 2>> $ERR else rsync $OPTIONS --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