#!/bin/bash
#
# systembackup script Ver0.1
# Copyright (C) 1998 Wataru Shito.
#
# Script for both full and incrimental backup.  This is for manual backup
# thus this is mainly written for system backup since system configuration
# does not change often.  If you do not have root priviledge, you have to
# change the code for the part that specifies where to save the archive.
#
# This program is free software with absolutely no warranty.
# You can redistribute it and/or modify it under the terms of GNU General
# Public License.
#
###########################################################################
# User defined environment.
#
# Example of ~/.backuprc
#
# The following example will create the archive:
#      "Simple_Home_Dir/home_wshito_Jul07_1998"
# if today is July 7, 1998.  
#
# Simple Home Dir:/home/wshito -exclude /home/wshito/documents /home/wshito/diary
# 
# You have to write a name of the archive and paths for including files 
# in a single line.
# The option "-exclude" tells program not to include the directories
# specified after the option into an archive.
# Thus it will create an archive of files and directories under /home/wshito
# except 'document' and 'diary' directories.
# The archive title is "Simple Home Dir" in this case.  Backed up archives 
# will be saved in a directory "Simple_Home_Dir".
# Archive file name can be specified by the user in a braces after the archive
# title.  Otherwise, the first path in a backing up directories will be used
# for an archive name.
# The specified name is used as a base name of the archive file, so the date
# of backup will be added at the end.
#
# The following example will create an archive "Diary/diary_Jul07_1998"
# instead of "Diary/home_wshito_diary_Jul07_1998".
#
# Diary(diary):/home/wshito/diary
# 
##########################################################################
# List of global variables
#
# $username
# $paren_dir	(This is the parent's dir's absolute path for all the archives)
# $user_choice
# $archive_list  (separator is three pipes '|||')
# $paths_list    (separator is three pipes '|||')
# $save_archive_here	(dir name for archive to save in)
# $archive_working_on
# $directories_working_on
# $excluding_directories
# $last_backup_file
##########################################################################
# List of functions
# 
# show_backup_sets()
# parse_user_choice()
# delete_dup_choice()
# check_setup()
# make_list()
# archive_destination()
# set_archive_filename()
# set_directories() ¤³¤³¤«¤é
# make_excluding_list()
# full_backup()
# find_newer_files()
# incrimental_backup()
# 
# and after these, main program is defined.
########################################################################
#
# Functions' definitions start here
#
########################################################################
# function: show_backup_sets
# usage: show_backup_sets $username
#
# This function show the list of backup sets and obtain the user choice
# and set the BACKUP_DIRECTORIES
#
show_backup_sets() {
    if [ "$1" = root ]; then
	setup_file=~root/.backuprc
    else
	setup_file=$HOME/.backuprc
    fi
    # Now, print all the setup names.
    echo 
    echo "Backup sets are listed as follows:"
    grep ".*:" $setup_file | cat -n > /tmp/backup_set_list
    echo "     0  All"
    echo "$(cut -d: -f1 /tmp/backup_set_list)"
    echo
    until [ -n "$backup_set_number" ]; do
	echo -n "Which backup set would you like to use? > "
	read backup_set_number
    done
}

####################################
# function: parse_user_choice()
# usage: parse_user_choice $backup_set_number
#
# Parse the user choice and put all the choice into $user_choice
#
parse_user_choice(){
    max_num=$( wc -l < /tmp/backup_set_list )
    for i in $*
    do
	if [ "$i" = "0" ]; then 
	    z=1
	    while [ "$z" -le "$max_num" ]; do
		user_choice="$user_choice $z"
		z=$(( $z + 1 ))
	    done
	    continue
        fi
	#
	# When the user put too much interval sign like 4--5 
	# or missing number like --8
	#
	z=${i%-*}
	if [ -z "$z" ]; then
	    echo "Error(systembackup): missing num1 in 'num1-num2' in your choice"
	    exit
 	fi
	if [ $z != ${z%-*} ]; then
	    echo "Error(systembackup): you put too many '-' in your choice"
	    exit
	fi
	#
	# When the user choice is interval such as 4-5.
	# $z is 4 and $zz is 5 in this case.
	#
	if [ "$z" != "$i" ]; then
	    zz=${i#*-}
	    if [ -z "$zz" ]; then
		echo "Error(systembackup): missing num2 in 'num1-num2' in your choice"
		exit
	    fi
	    if [ $z -gt $zz ]; then
		echo "Error(systembackup): must be num1 < num2 in num1-num2"
		exit
	    fi
	    while [ \( $z -le $zz \) -a \( $z -le $max_num \) ]
	    do
		user_choice="$user_choice $z"
		z=$(( $z + 1 ))
	    done
	    continue
	fi
	user_choice="$user_choice $i"
    done
}


###########################################
# function: delete_dup_choice()
# usage: delete_dup_choice $user_choice
#
# Remove the duplicated choices after parsing 
# and put the result in the global variable 'user_choice'.
#
delete_dup_choice(){
    local pop stack choice

    stack="$* "
    for pop in $*
    do
        stack=" ${stack#* }"
	if [ "${stack% ${pop} *}" = "$stack" ]; then
	    choice="$choice$pop "
	fi
	stack=${stack# }
    done

    user_choice=${choice# }
}

###########################################
# function: check_setup
#
# This checks the format of paths user set.
#
check_setup(){
    local num retrived_line answer
    echo
    echo "Your choice is:"
    for num in $*
    do
	retrived_line=$(grep "    $num	" /tmp/backup_set_list)
	echo "$retrived_line"
	retrived_line="${retrived_line#*:}"

	if [ "$retrived_line" = "" ]; then
	    echo "Error: Please set the paths for the directories you would like to backup."
	    exit
	fi
	if [ "${retrived_line%%:*}" = "" ]; then
	    echo "Error: Please remove the colon `:' at the beggining of the path you set."
	    echo "       The line below is what you have set:"
	    echo "       $retrived_line"
	    exit
	fi
	if [ "${retrived_line##*:}" = "" ]; then
	    echo "Error: Please remove the colon `:' at the end of the path you set."
	    echo "       The line below is what you have set:"
	    echo "       $retrived_line"
	    exit
	fi
    done

    echo
    answer=""
    while [ \( "$answer" != "y" \) -a \( "$answer" != "n" \) ]
    do
	echo -n "Are you sure you want to work on the directories above? [y/n] > "
	read answer
	if [ "$answer" = "n" ]; then
	    echo "Please start from the beginning."
	    exit
	fi
    done
}

####################################
# function: make_list
# usage: make_list $choice_num
#
# Make two lists of archive names, backup directories. 
#
# Global Variable:
#    $archive_list is the list of archive name in order.
#    $paths_list is the list of paths to backup.
#    each element is separated with three '|' which is specified as
#    \|\|\| in the shell since the pipe '|' is a special character.
#
make_list(){
    local num retrived_line
    archive_list=""
    paths_list=""
    for num in $*
    do
	retrived_line=$(grep "    $num	" /tmp/backup_set_list)
	paths_list="$paths_list${retrived_line#*:}|||"

	# Retriving the set name with the number.
	retrived_line="${retrived_line%%:*}"  
	# Remove the number. 
	retrived_line="${retrived_line#*	}"
	# Check the syntax for parenthesis.
	if [ \( "${retrived_line#*(}" != "$retrived_line" \) -o  \
	     \( "${retrived_line%)}" != "$retrived_line" \) ]; then
	    if [ -z "${retrived_line#*()*}" ]; then
		echo "Error: empty parenthesis for the archive file name."
		exit
	    fi
	    if [ -z "${retrived_line#*(* *)*}" ]; then
		echo "Error: don't put spaces in parenthesis for the archive file name."
		exit
	    fi
	    if [ "${retrived_line#*(*(*}" != "$retrived_line" ]; then
		echo "Error: too much '(' in archive name."
		exit
	    fi
	    if [ "${retrived_line%*)*)*}" != "$retrived_line" ]; then
		echo "Error: too much ')' in archive name."
		exit
	    fi
	    if [ "${retrived_line%%(*}" = "" ]; then
		echo "Error: missing archive title before parenthesis."
		exit
	    fi
	    if [ "${retrived_line##*)}" != "" ]; then
		echo "Error: don't put anything after parenthesis."
		exit
	    fi
	fi
	# Replace any spaces and back slash with underscore.
	archive_list="$archive_list$(echo "$retrived_line" | tr ' ' _ | tr / _ )|||"
    done
    archive_list=${archive_list%|||}
    paths_list=${paths_list%|||}
}

#########################################################################
# function archive_destination
# usage: archive_destination
# 
# This uses the global variables: $username, $archive_list, $paths_list
#
# IF root: Check if there is a directory called "/backup/system_backups".
#          If there is not, create one to save archives in.
# IF user: Check if there is a directory called "/user_backup/$USER_backup".
#          If there is not, create one to save archives in.
archive_destination() {
    local answer archive_name
    # Set the parent's directory for each archive.
    if [ "$username" = root ]; then
	paren_dir="/backup/system_backup"
    else
	paren_dir="/user_backup/${username}_backups"
    fi

    #
    # setting parent's dir for each archive.
    #
    if [ ! -e "$paren_dir" ]; then
	echo "No directory: $paren_dir"
	answer=""
	while [ \( "$answer" != "y" \) -a \( "$answer" != "yes" \) ]
	do
	    echo -n "Do you want me to create one? [y/quit] > "
	    read answer
	    if [ "$answer" = "quit" ]; then
		exit
	    fi
	done
	echo "Creating the directory, ${paren_dir}."
	mkdir $paren_dir
	if [ ! -e "$paren_dir" ]; then
	    echo "Error: \"${paren_dir}\" could not create the directory."
	    exit
	fi
    else
	if [ ! -d "$paren_dir" ]; then
	    echo "Error: Please rename the file \"${paren_dir}\" "
	    exit
	fi
    fi

    #
    # setting directories for each archive name.
    # $archive_name is a local variable.
    #
    IFS="|||"
    for archive_name in $archive_list
    do
	archive_name=${archive_name%%(*}
	if [ ! -e "${paren_dir}/$archive_name" ]; then
	    echo
	    echo "No directory: ${paren_dir}/$archive_name"
	    answer=""
	    while [ \( "$answer" != "y" \) -a \( "$answer" != "yes" \) ]
	    do
		echo -n "Do you want me to create one? [y/quit] > "
		read answer
		if [ "$answer" = "quit" ]; then
		    exit
		fi
	    done
	    echo "Creating the directory, ${paren_dir}/$archive_name"
	    mkdir ${paren_dir}/$archive_name
	    
	else
	    if [ ! -d ${paren_dir}/$archive_name ]; then
		echo "Error: Please rename the file \"${paren_dir}/$archive_name\""
		exit
	    fi
	fi
    done
    unset IFS
}

##########################################################################
# function: set_archive_filename 
# useage: set_archive_filename $one_archive_name|||$paths_for_the_archive
# using global variables: $backup_type 
#     
# This sets two global variables: $archive_working_on and $save_archive_here
# set_archive_filename() takes an archive name which is specified in a 
# ~/.backuprc file and make a file name out of it.  If an user put a name
# in braces it will be used as a file name without modification.  If an user
# didn't specify the archive file name, the first path in a archive will be
# the archive file name.
#
set_archive_filename() {
    local archive_name backup_type_suffix paths
    archive_name="$*"
    paths=${archive_name#*|||}
    archive_name=${archive_name%%|||*}
    today=$(date +%b%d_%Y)

    if [ "$backup_type" = 1 ]; then
	backup_type_suffix=incri
    else
	backup_type_suffix=full
    fi
    if [ "$archive_name" != "${archive_name%)*}" ]; then
	save_archive_here=${archive_name%%(*}
	archive_name=${archive_name%)*}
	archive_name=${archive_name#*(}
	archive_name=${archive_name}_${today}.${backup_type_suffix}
    else
	save_archive_here=$archive_name
	paths=${paths%% *}      # the first path.
	paths=${paths#/}	# Remove the first backslash `/' from the path.
	paths=$(echo "$paths" | tr / _ ) # Replace `/' with `_'
	archive_name=${paths}_${today}.${backup_type_suffix}
    fi
    
    # archive_working_on is a global variable.
    archive_working_on=$archive_name
}


##########################################################################
# function: set_directories
# useage: set_directories $list_of_paths_for_an_archive
#
# This sets the global variable $directories_working_on and 
# $excluding_directories
#
set_directories() {
    local dir_list
    dir_list=$*
    directories_working_on="${dir_list% -exclude*}"
    # Now, set the excluding file or directory lists.
    if [ "$dir_list" != "$directories_working_on" ]; then
	excluding_directories="${dir_list#*-exclude }"
    else
	excluding_directories=""
    fi
}


################################################################
# function: make_excluding_list
# usage: make_excluding_list
#
# Excluding list must be the absolute path.
# This accesses the global variable $excluding_directories.
# $excluding_directories contains the list of excluding files and dirctories
# separated by a space for each archive.
#
make_excluding_list () {
    if [ -e /tmp/excluding_these ]; then
	rm /tmp/excluding_these	
    fi
    while [ "$1" != "" ]
    do
	find $1 -print >> /tmp/exclude_these
	shift
    done
}


################################################################
# function: full_backup
# usage: full_backup
#
# This accesses three global variables: $save_archive_here
#                                       $archive_working_on
#                                       $directories_working_on
#                                       $excluding_directories
#
full_backup() {
    local exclude_option

    # Make the excluding files list.
    if [ "$excluding_directories" != "" ]; then
	make_excluding_list $excluding_directories
	exclude_option="-X /tmp/exclude_these"
    else
	exclude_option=""
    fi

    # In the `tar' command, do not quote $directory_working_on with double quote.
    tar -cMf ${paren_dir}/$save_archive_here $exclude_option --total $directories_working_on
    
    if [ "$exclude_option" != "" ]; then
	rm /tmp/exclude_these
    fi
}


###########################################################################
# function: find_newer_files
# usage: find_newer_files $directory_working_on
# 
# This accesses the global variables: $last_backup_file
#
# This function get a list of directories that each path is separated
# by a space and find files created newer than the file `/etc/last_backup'
# or `$HOME/last_backup' ( = $last_backup_file).
# The result of searching files will be saved in the file /tmp/backup.
# Each path should be put in a single line in /tmp/backup.
#
# The existance of the file will not checked here.  It should be
# checked before `backup' scripts start in the function `check_setup()'.
#
find_newer_files() {
    if [ -e /tmp/back_these_up ]; then
	rm /tmp/back_these_up
    fi
    while [ "$1" != "" ]
    do
	find "$1" -cnewer $last_backup_file -type f -print >> /tmp/back_these_up
	shift
    done
}

################################################################
# function: incrimental_backup
# usage: incrimental_backup
#
# This accesses three global variables: $save_archive_here
#                                       $archive_working_on
#                                       $directories_working_on
#                                       $excluding_directories
#
incrimental_backup() {
    local exclude_option

    # This will make /tmp/back_these_up with list of files to be backed up
    # that were modifed or created since the last backup.		
    find_newer_files $directories_working_on
    # Make the excluding files list.
    if [ "$excluding_directories" != "" ]; then
	make_excluding_list $excluding_directories
	exclude_option="-X /tmp/exclude_these"
    else
	exclude_option=""
    fi

    tar -cMf ${paren_dir}/$save_archive_here $exclude_option --total -T /tmp/back_these_up

    rm /tmp/back_these_up
}

		
##################################################################
#
#	Main program starts here.
#
##################################################################

# First of all, list the backup sets and let user to choose one.
username=$(whoami)

# If nothing is mounted on /backup quite this script.
if [ "$username" = root ]; then
    checkmount=$(mount |  grep /backup)
    if [ -z "$checkmount" ]; then
	echo "Error(systembackup): Please mount the backup disk on /backup."
	exit
    fi
else
    checkmount=$(mount |  grep /user_backup)
    if [ -z "$checkmount" ]; then
	echo "Error(systembackup): Please mount the backup disk on /user_backup."
	exit
    fi
fi


show_backup_sets $username
parse_user_choice $backup_set_number
delete_dup_choice $user_choice

# Secondly, check the user environment setup is correct.
check_setup $user_choice

make_list $user_choice

# Set the directories to save the backup archives based on the user.
# This uses the global variables $archive_list and $paths_list
archive_destination

## Starting the backup from here.
#
#  Preferebly, arichive file should have a name with the date.
#  After the archive file gets three versions, the oldest one should
#  be removed and new one should be added.  So there will be always a
#  three archives for each directory but with different day.
#
## First, ask either full backup or incrimental backup.
#  if the choice is full backup, confirm it.
#
answer=n
while [ \( "$answer" = "n" \) -o \( -z "$answer" \) ] ; do
    echo
    echo "Choose the number from the below:"
    echo "  1. Incrimental backup (Default)"
    echo "  2. Full backup"
    echo "  3. Quit"
    echo
    echo -n " > "
    read backup_type

    if [ "$backup_type" = "1" ] || [ "$backup_type" = "" ] ; then
	echo -n "Are you sure you want to do the incrimental backup? [y/n] > "
	read answer
	continue
    fi
    if [ "$backup_type" = "2" ]; then
	echo -n "Are you sure you want to do the full backup? [y/n] > "
	read answer
	continue
    fi
    if [ "$backup_type" = "3" ] || [ "$backup_type" = "q" ]; then
	echo -n "Are you sure you want to quit backup? [y/n] > "
	read answer
	if [ "$answer" = "y" ]; then
	    exit
	fi
    fi
done

echo
if [ "$backup_type" = "1" ]; then
    echo "Starting the incrimental backup."
else
    echo "Starting the full backup."
fi

counter=1
while [ "$tmp_a" != "$archive_list" ]
do
    tmp_a="${archive_list%%|||*}"
    tmp_p="${paths_list%%|||*}"
    archive_list="${archive_list#*|||}"
    paths_list="${paths_list#*|||}"
    set_archive_filename "${tmp_a}|||${tmp_p}"    
    # $archive_working_on and $save_archive_here is ready.
    set_directories $tmp_p    
    # $directories_working_on and $excluding_directories are ready too.
    echo "Backing up ${counter}	: ${tmp_a}/${archive_working_on}"

    if [ "$backup_type" = "2" ]; then
	full_backup
    else
	# Make sure the full backup was taken before.
	if [ "$(whoami)" = root ]; then
		last_backup_file=/etc/last_backup
	else
		last_backup_file=$HOME/last_backup
	fi
	if [ ! -e "$last_backup_file" ]; then
		echo "Error: I cannot find $last_backup_file "
		echo "       The last backup date cannot be specified."
		echo "       I suggest you to do the full backup first."
		exit
	fi
	incrimental_backup
    fi
    counter=$(( $counter + 1 ))
done

if [ "$username" = root ]; then
    touch /etc/last_backup
else
    touch $HOME/last_backup
fi

## Checking the free disk space..
#
#  Get the summary with kilobytes.
if [ "$username" = root ]; then
	freespace=$(( $(du -sk /backup | cut -f1) / 1000 ))MB
	echo 
	echo "Finished the system backup"
	echo $(du -sk /backup)
	echo "Approximately $freespace used."
else
	freespace=$(( $(du -sk /user_backup/${username}_backups | cut -f1) / 1000 ))MB
	echo 
	echo "Finished the user backup"
	echo "Your backup directory is using this disk:"
	echo "	$(du -sk /user_backup/${username}_backups)"
	echo "	Approximately $freespace used."
	echo
	echo "The disk containing your backup  directory is used :"
	echo "	$(du -sk /user_backup/${username}_backups)"
	echo "	Approximately $freespace used."
	
fi

rm /tmp/backup_set_list
