#!/bin/bash # NAME: # backup2cd # DESCRIPTION: # an attempt to automate creating CD-sized backup lists and ISOs # AUTHOR: # copyright (C) 2003 Terry Vessels # LICENSE: # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2 dated # June 1991, as published by the Free Software Foundation. # # 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # NOTE: Copyright of the GNU General Public License, version 2, dated # June 1991, is retained by the Free Software Foundation. # Copyright of this program is retained by Terry Vessels. You are granted # the rights to copy, modify and distribute this program only under the # terms of the GNU General Public License version 2. See the file GPL.txt # which you should have received with this program. # # END OF LICENSE. # # LOCATION: # At the time of this writing, the complete distribution of this program # is available at # http://edge-op.org/files/backup2cd.tar.gz # ######################################################################### # # depends on: # 1. for the lists: # awk, bash, bc, cd, cp, date, df, du, grep, pwd, rm, sed, split # 2. for the "ISO" image files: mkisofs # 3. for recording the ISOs to CDs: cdrecord # version=0.20.1 # # I. Set variables # A. default values # B. ask user: # 1. maximum bytes per CD ($max_bytes = $max_MB * 1024 * 1024) # 2. top level directory to be backed up ($backup_root) # 3. working directory for list generation ($arc_dir) # 4. cdrecord parameters ($cd_param) # C. determine real path if $backup_root is a symlink # # II. Functions # A. msg_out: send a message to screen and to a logfile # B. msg_decor: just a way to break up some of the feedback to the user # C. intro: tell what|who|why # D. link_check: test a directory for symlink, adjust to real path # E. compare: uses bc to compare file or directory size to $max_bytes # F. get_avail: uses df to get the available space in bytes # G. file_move: if there is enough space, move a file # H. set_vars: initialize variables # I. list_maker: creates two lists - oksized and oversized # J. big_file: a rambling mess to deal with individual files which # are larger than $max_bytes and therefore won't fit on one CD # K. cd_lists: sort oksized list by sizes, create cd### lists # L. iso_maker: create directory, copy files to it, mkisofs from it # M. cd_burner: prompt user while recording a CD for each iso # # III. Create file lists # A. Check for old lists, left over from previous run, delete # B. set $n = $backup_root, call list_maker $n: # C. get size of $n # D. compare to $max_bytes # E. if smaller, add size in bytes and name to list 'oksized' # F. if larger, add to list 'oversized' # G. if larger and a directory, then for each item in the directory, # call list_maker # H. if larger and a file, call big_file to see if it can be split # I. when list_maker finishes, sort 'oksized' with largest first # J. set cd_count to 1, bytes to 0, begin creating cd lists: # K. any files left to backup? # 1. YES: does current file fit current CD? # a. YES: add to current CD list; K. # b. NO: is it the last file in the list? # 1. YES: next CD; K. # 2. NO: next file in list; K.1. # 2. NO: done # L. when all CD lists are finished, use awk to create lists suitable # for tar or mkisofs, by stripping the sizes from each line # # IV. Create ISO images (ask user first) # A. create a temporary working directory within $arc_dir # B. copy files from cd list to the temporary directory # C. use mkisofs -r -J # # V. Burn cds (ask user first) # A. prompt for blank CD # B. use cdrecord # C. ask user about deleting the ISO # ######### # default variables default_max_MB=675 default_backup=/ default_arc_dir=/tmp/cdrbackup default_cd_param="-eject -v speed=12 dev=/dev/hdc -data" # safe_margin is number of bytes less than max_bytes for split # because du and ls give different values safe_margin=1000000 # set the dotglob shell variable so files and directories # beginning with '.' are included in all tests and lists shopt -s dotglob # functions function msg_out()\ { # send some message to the screen and to the log _msg="$1" if [ "${_msg}" != "" ] then if [ -f "${arc_dir}"/start-time-${start_time} ] then echo "${_msg}" >> "${arc_dir}/log-${start_time}" fi echo "${_msg}" fi return } function msg_decor()\ { # just a way to make things a bit more readable for the user _msg="$1" if [ "${_msg}" != "" ] then msg_out "${_msg}" fi msg_out "************************************************************" return } function intro()\ { # tell wtf, wtf, wtf # wtf msg_decor msg_out "This is backup2cd version ${version}" # wtf msg_decor "Cobbled together by Terry Vessels" # wtf msg_out "The purpose is to produce lists of files and directories" msg_out "to be backed up to CD, while not going over some maximum" msg_out "size you specify." msg_out " >If a *directory* is oversized, its contents are examined and" msg_out " will be shared among as many CD lists as needed." msg_out " >If a file is oversized, and you have 'split' installed, you" msg_out " will be offered the option to split the file among CDs." msg_out "Full paths are preserved so that files may be copied from CD" msg_out "back to their original locations." msg_out # FIX THIS! msg_out "This program does not clean up after itself. The lists it" msg_out "generates remain after you exit, normally or otherwise." msg_out "Running the program again and setting the variables to what" msg_out "they were before, will therefore allow you to continue." msg_out "The disadvantage is that if you generate the CD lists" msg_out "multiple times in the same work directory, you will have a" msg_out "mess in each list. You have to decide what to delete from" msg_out "your working directory." msg_decor read -p "Press Enter when ready to continue. \$> " USER_IN msg_decor return } function link_check()\ { # check to see if a directory is a symlink, adjust for it _check="$1" if [ -h "${_check}" ] then msg_out ${_check} is a symlink _pdir=$(pwd) cd -P "${_check}" _check=$(pwd) cd "${_pdir}" msg_decor "real directory is ${_check}" fi } function compare()\ { # given two numbers, $c_res=1 if $_num1 > $_num2 _num1=$1 _num2=$2 c_res=$(echo "scale=1; ${_num1} > ${_num2}" | bc) return } function get_avail()\ { # given a path, $b_av = bytes available _dir="$1" b_av=$(df --block-size=1 "${_dir}" | tail -1 | awk '{print $4}') return } function file_move()\ { # given a filename and path (source and destination), # check space and move if possible _src="$1" _dest="$2" src_size=$(du --block-size=1 --summarize "${_src}" | awk '{print $1}') msg_decor get_avail "${_dest}" msg_out "There are ${b_av} bytes available on ${_dest}" msg_out "${src_size} bytes are needed." compare ${b_av} ${src_size} if [ ${c_res} -gt 0 ] then msg_decor "There IS enough room." mv -v "${_src}" "${_dest}" fi return } function set_vars()\ { # SET VARIABLES: msg_decor start_time=$(date +%Y-%m-%d-%H.%M.%S) msg_decor "start-time = ${start_time}" # maximum bytes per CD c_res=0 until [ ${c_res} -gt 0 ] do msg_out "What is the maximum capacity (in MB) of your CDRs?" msg_out "default = ${default_max_MB}" msg_out "REMEMBER: Besides the data, a standard CD includes" msg_out "a table of contents which depends on how many files are recorded." read -p "(620 *should* fit on 650MB CDs, 668 on 700MB CDs) \$> " USER_IN max_MB=${USER_IN:-${default_max_MB}} max_bytes=$(echo "scale=1; ${max_MB} * 1024 * 1024" | bc) compare ${max_bytes} 0 done msg_decor "max_MB = ${max_MB} max_bytes = ${max_bytes}" # highest level directory to be included in backup b_r="" until [ -d "${b_r}" ] do msg_out "What is the full path to the top level directory you wish to back up?" msg_out "Example 1: / Example 2: /mnt/windows" msg_out "default = ${default_backup}" read -p "\$> " USER_IN backup_root=${USER_IN:-${default_backup}} msg_out "backup_root = ${backup_root}" msg_out "Checking for symlink..." link_check "${backup_root}" backup_root="${_check}" b_r="${backup_root}" done msg_decor "backup_root = ${backup_root}" # working directory t_z="" until [ -f "${t_z}" ] do msg_out "Where do you want the CD file lists and (optionally) ISO files created?" msg_out "You need write permission in the directory, and it should not be" msg_out "within the backup directory." msg_out "default = ${default_arc_dir} " read -p "\$> " USER_IN arc_dir=${USER_IN:-${default_arc_dir}} msg_out "arc_dir = ${arc_dir}" msg_out "Checking for symlink..." link_check "${arc_dir}" arc_dir="${_check}" mkdir -p "${arc_dir}" if [ -f "${arc_dir}"/start-time-* ] then rm "${arc_dir}"/start-time-* fi touch "${arc_dir}"/start-time-${start_time} t_z="${arc_dir}"/start-time-${start_time} done msg_decor msg_out "Log begins: This is backup2cd version ${version}" # wtf msg_decor "Cobbled together by Terry Vessels" msg_decor msg_decor "arc_dir = ${arc_dir} start time = ${start_time}" # cdrecord parameters msg_out "What parameters should be passed to cdrecord, if the CDs will be recorded?" msg_out "(the ISO filename will be appended later)" msg_out "default = ${default_cd_param}" read -p "\$> " USER_IN cd_param="${USER_IN:-${default_cd_param}}" msg_decor "cd_param = ${cd_param}" # # end SET VARIABLES # } function list_maker()\ { if [ ! -f "${arc_dir}"/start-time-${start_time} ] then msg_out "You must first set variables before making a list." return fi # $_name is a file or directory _name="$1" _size=$(du --block-size=1 --summarize "${_name}" | awk '{print $1}') compare ${_size} ${max_bytes} msg_decor "_name = ${_name} _size = ${_size} compare = ${c_res}" if [ ${c_res} -gt 0 ] then echo ${_size} ${_name} >> "${arc_dir}/oversized" msg_decor "ADDED ${_size} ${_name} to ${arc_dir}/oversized" if [ -d "${_name}" ] then for n in "${_name}"/* do msg_decor " calling list_maker ${n} " list_maker "${n}" done elif [ -f "${_name}" ] then msg_decor " calling big_file ${_name} ${_size}" big_file "${_name}" ${_size} fi else echo ${_size} ${_name} >> "${arc_dir}/oksized" msg_decor "ADDED ${_size} ${_name} to ${arc_dir}/oksized " fi return } function big_file() \ { # this is a big, ugly, rambling mess # what to do when encountering a single file that is # too big for a single CD big_one="$1" big_size="$2" big_path=${big_one%/*} big_name=${big_one##*/} test_split=$(which split) msg_decor msg_out "ERROR:" msg_out " ${big_one}" msg_out " is a single file, larger than ${max_bytes}" msg_out "Available courses of action: " msg_out " => exit" msg_out " => skip ${big_one}" if [ -f ${test_split} ] then msg_out " => split ${big_one}" fi msg_decor read -p "Do you want to continue? y/n \$> " USER_IN if [ ${USER_IN} == "n" ] then msg_decor "Bye. Hope you fix it." exit fi if [ -f ${test_split} ] then msg_decor msg_out "max_bytes ${max_bytes}" msg_out "safe_margin ${safe_margin}" parts_size=$(echo "scale=1;${max_bytes}-${safe_margin}"|bc) msg_out "parts_size ${parts_size}" msg_decor msg_out "Possible options:" msg_out " => split the file where it is now," msg_out " => move it elsewhere and put the split parts back in its place" msg_decor msg_decor "First, checking available space ..." msg_out "${big_size} bytes are needed for the split parts" get_avail "${backup_root}" msg_out "There are ${b_av} bytes available on ${backup_root}" compare ${b_av} ${big_size} if [ ${c_res} -gt 0 ] then msg_out "There is room enough on " msg_out " ${backup_root}" msg_out " to split" msg_out " ${big_one}" msg_out "The original file will not be harmed." msg_out "Do you want to split" msg_out "${big_one}" read -p " now, where it is? y/n " USER_IN if [ ${USER_IN} == "y" ] then split -b ${parts_size} "${big_one}" "${big_one}_split-" ls -l "${big_one}"* msg_decor read -p "hit Enter to continue \$> " USER_IN for n in "${big_one}_split-"* do msg_out " calling list_maker ${n} " list_maker "${n}" done fi else msg_out "There is not enough room on ${backup_root}" msg_out "to split ${big_one}" msg_out "An alternative is to move ${big_one} elsewhere," msg_out "split it, putting the split parts back into the" msg_out "${big_path} directory." msg_decor read -p "Do you want to see the output of df ?" USER_IN if [ ${USER_IN} == "y" ] then df -h fi msg_decor msg_out "Do you want me to move" msg_out " ${big_one}" msg_out " elsewhere, split it, then move the parts back into " msg_out "${big_path}" read -p "where the original file was located? y/n \$>" USER_IN msg_decor if [ ${USER_IN} == "y" ] then new_big="" until [ -f "${new_big}" ] do msg_out "To what directory do I move" msg_out "${big_one} ?" read -p "\$> " move_dir while [ ! -d "${move_dir}" ] do msg_out "${move_dir} is not a directory" msg_out "To what directory do I move" msg_out "${big_one} ?" read -p "\$> " move_dir done file_move "${big_one}" "${move_dir}" new_big="${move_dir}/${big_name}" done split --bytes=${max_bytes} "${new_big}" "${big_one}_split-" ls -l "${big_one}"* read -p "hit Enter to continue \$> " USER_IN for n in "${big_one}_split-"* do msg_out " calling list_maker ${n} " list_maker "${n}" done else msg_out "I don't know what else to do but exit" exit fi fi fi return } function cd_lists()\ { # fiddle with the file list if [ -f "${arc_dir}"/oksized ] then sort -nr "${arc_dir}"/oksized > "${arc_dir}"/oksized.rev else msg_out "\"oksized\" list NOT found; generate it and try again." return fi # begin breaking the list into multiple lists, # each containing $max_bytes of files f_list="${arc_dir}/oksized.rev" cd_count=1 cd_num=001 file_count=$(grep -c $ "${arc_dir}"/oksized.rev) cur_bytes=0 rem_bytes=${max_bytes} file_count=$((${file_count})) n=1 # any files left? while [ ${file_count} -gt 0 ] do # does the current file fit on the current CD? cur_file=$(sed -n ${n}p "${f_list}") cur_fname=$(echo "${cur_file}" | awk '{name=substr($0, index($0, $2)); print name}') #' file_size=$(sed -n ${n}p "${f_list}" | awk '{print $1}') # # test_bytes=$(echo "scale=1; ${cur_bytes} + ${file_size}" | bc) # test_size=$(echo "scale=1; ${max_bytes} >= ${test_bytes}" | bc) c_res=0 compare ${rem_bytes} ${file_size} msg_out "rem_bytes=${rem_bytes} file_size=${file_size} c_res=${c_res}" if [ ${c_res} -gt 0 ] then # it fits, add it to the current CD list cur_bytes=$(echo "scale=1; ${cur_bytes} + ${file_size}" | bc) rem_bytes=$(echo "scale=1; ${max_bytes} - ${cur_bytes}" | bc) cur_file=$(sed -n ${n}p "${f_list}") echo "${cur_file}" >> "${arc_dir}"/cd${cd_num} msg_decor " ADDING FILE ${cur_file} TO cd${cd_num}" # strip the file from the main list awk -v cur_rec=${n} '{ if (FNR != cur_rec) { print $0 } }' "${f_list}" > "${arc_dir}"/oksized.temp cp -f "${arc_dir}"/oksized.temp "${arc_dir}"/oksized.rem f_list="${arc_dir}/oksized.rem" file_count=$(grep -c $ "${f_list}") n=1 else # the file doesn't fit, try the next one n=$((n + 1)) # end of the list? if [ $n -gt ${file_count} ] then # pad the count with zeros for the cd name cd_count=$((cd_count + 1)) cd_num=${cd_count} if [ ${cd_count} -lt 100 ] then cd_num=0${cd_count} fi if [ ${cd_count} -lt 10 ] then cd_num=0${cd_num} fi cur_bytes=0 rem_bytes=${max_bytes} n=1 msg_out " cd_count ${cd_count} cd_num ${cd_num}" fi fi done # make the cd lists suitable for sending to tar or mkisofs for n in "${arc_dir}"/cd* do awk '{name=substr($0, index($0, $2)); print name}' "${n}" > "${n}".list done } function iso_maker()\ { # get some prefs from the user # options: 1. record each ISO as it is finished # 2. delete each ISO # 1. create, record, delete # 2. create, record # 3. create, delete (makes no sense) # 4. create USER_IN="" until [ "${USER_IN}" == "y" ] do msg_out "Each ISO created will, of course, be about ${max_MB}" msg_out "Consider available space when choosing your option --" msg_out "Your options:" msg_out " create, record immediately, delete immediately" msg_out " create, record immediately, keep" msg_out " create, keep (record later)" msg_out " (if keeping, you can choose to be prompted" msg_out " after each iso is made, or not)" msg_out "Will you want to record each ISO to a CD, using" msg_out " ${cd_param}" msg_out "as parameters for cdrecord," read -p "as each ISO is created? y/n \$> " rec_iso read -p "Will you be keeping all ISO files? y/n \$> " sav_iso read -p "Do you want a pause after each ISO is created? y/n \$> " prompt_iso msg_out "You chose:" msg_out " Record each as finished = ${rec_iso} " msg_out " Keep all ISO image files = ${sav_iso} " msg_out " Prompt between ISO files = ${prompt_iso} " read -p "Is this correct? y/n \$> " USER_IN done # in case the junk directory exists, and has been filled before: if [ -d "${arc_dir}"/junk ] then msg_out "Need to clear out any old files before making new ISOs" chmod -R 755 "${arc_dir}"/junk rm -rf "${arc_dir}"/junk/* else # in case it doesn't already exist: mkdir -p "${arc_dir}/junk" fi # create a duplicate directory structure under ${arc_dir}, from one list # copy the files from a cd list to that duplicate directory for n in "${arc_dir}"/cd*.list do # first, clean up from the last ISO msg_out "Cleaning up ..." chmod -R 755 "${arc_dir}"/junk rm -rf "${arc_dir}"/junk/* f_list="${n}" f_count=$(grep -c $ "${f_list}") for x in $(seq ${f_count}) do cur_file=$(sed -n ${x}p "${f_list}") # get the path name from the current file p_name=${cur_file%/*} dest_name="${arc_dir}/junk${p_name}" mkdir -vp "${dest_name}" msg_out "Copying files... this may take a while" cp -a "${cur_file}" "${dest_name}" done mkisofs -R -J -o "${n}".iso "${arc_dir}"/junk msg_out "${n}.iso FINISHED" if [ ${rec_iso} == "y" ] then msg_out "parameters to be passed to cdrecord:" msg_out "${cd_param} ${n}.iso" read -p "Get a blank CD ready, then press ENTER" USER_IN cdrecord ${cd_param} "${n}.iso" fi if [ ${sav_iso} == "n" ] then rm -iv "${n}.iso" fi if [ ${prompt_iso} == "y" ] then msg_out "paused after ${n}.iso --" read -p "Press ENTER when ready to continue" USER_IN fi done } function cd_burner()\ { for n in "${arc_dir}"/*.iso do read -p "Do you want to record ${n} to a CD \ using \"${cd_param}\" as parameters for cdrecord? y/n " USER_IN if [ ${USER_IN} == "y" ] then read -p "Get a blank CD ready, then press ENTER" USER_IN cdrecord ${cd_param} "${n}" fi read -p "Remove ${n} ? y/n " USER_IN if [ ${USER_IN} == "y" ] then rm -iv "${n}" fi done } # # MAIN # # status flags and messages nl=" " _m1="UN" # 'select' was abandoned because I couldn't get it to make dynamic menus #select x in "${menu1}" "${menu2}" "${menu3}" "${menu4}" "${menu5}" "${menu6}"; intro while [ 1 ] do if [ -f "${arc_dir}"/start-time-${start_time} ] then _m1="" _m1A="${nl} = ${max_MB}MB = ${max_bytes}" _m1B="${nl} = ${backup_root}" _m1C="${nl} = ${arc_dir}" if [ -f "${arc_dir}"/oksized ] then _m2="${nl} (status = COMPLETED)" cd_lists=$(ls -1 "${arc_dir}"/cd[0-9][0-9][0-9] | wc -l) if [ ${cd_lists} -gt 0 ] then _m3="${nl} (status: CD lists exist)" fi else _m2="${nl} (status = NOT CREATED)" _m3="" fi iso_cnt=$(ls -1 "${arc_dir}"/*.iso | wc -l) if [ ${iso_cnt} -gt 0 ] then _m4="${nl} (status = ISO files exist; unknown if from lists)" else _m4="" fi else _m1="UN" _m1A="" _m1B="" _m1C="" fi msg_out "1) Set variables (status = ${_m1}SET): A. maximum size for files and directories (CD capacity) ${_m1A} B. top level directory to include in backup ${_m1B} C. working directory in which to write lists ${_m1C}" msg_out "2) Generate list of files and directories ${_m2} It begins with the directory you specify in 1.B. above. If that is larger than 1.A., the contents are checked. When finished, everything within 1.B. is included, but each item in the list will be less than the max specified in 1.A. Each item will, at worst, fit on 1 CD." msg_out "3) Generate a list for each CD from the above list ${_m3} Takes the list from above and creates as many individual lists as needed to include all items without exceeding the max size from 1.A. The lists are named cd###.list, where ### goes from 001 to 999." msg_out "4) Make an ISO image file from each of the above CD lists ${_m4}" msg_out "5) Record CDs from each ISO made above" msg_out "6) Exit" read -p "Pick a number \$> " REPLY case ${REPLY} in 2) msg_out "Generating list, beginning at ${backup_root}" msg_decor "Be patient..." list_maker "${backup_root}";; 3) cd_lists;; 4) iso_maker;; 5) cd_burner;; 6) end_time=$(date +%Y-%m-%d-%H.%M.%S) if [ -f "${arc_dir}"/end-time-* ] then rm "${arc_dir}"/end-time-* fi touch "${arc_dir}"/end-time-${end_time} msg_decor "${end_time}" exit;; 1 | *) set_vars;; esac done # an added attraction for those who are not easily offended by bad ascii art # +-------+ # | Begin | # +-------+ # | # +-----------------------------------------------------------+ # | Point to first line in 'oksized' (first file or directory)| # +-----------------------------------------------------------+ # | # +-------------------+ # | Set cd count to 1 | # +-------------------+ # | # +------------------------------------+ # | Get count of files and directories | # +------------------------------------+ # | # +----->(0) # | | # | ^ # | / \ # | / \ # | / any \ # | / files \ No +------+ # | < left? >---->| DONE | # | \ / +------+ # | \ / # | \ / # | \ / # | v # | |Yes # | | # | v # | (1)<-----------------------------+ # | | | # | v | # | ^ ^ | # | / \ / \ | # | / \ / \ | # | / \ / \ | # | / file \ No / last \ No +-----------+ # | < fits >---->< file, >---->| Next file | # | \ on / \ this / +-----------+ # | \ CD / \ list/ # | \ ? / \ ? / # | \ / \ / # | v v # | |Yes |Yes # | | | # | v v # | +----------+ +---------+ # | | Add file | | Next CD | # | | to list | +---------+ # | | for this | | # | | CD | +---------------------+ # | +----------+ | set byte count to 0 | # | | +---------------------+ # | +-----------+ | # | | Add file- | +----------------------------------+ # | | size to | | point to first file in next list | # | | byte count| +----------------------------------+ # | +-----------+ | # | | | # +------(2)<-------------+ #