#!/usr/bin/env zsh # # The MIT License (MIT) # # Copyright (c) 2014 Von Random # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # self_name=$0 # some hardcoded defaults default_cfg='/etc/backup.zsh.cfg' default_postfix=$(date +%F-%H%M) default_ftp_port='21' default_ssh_port='22' # echo to stderr function err { [[ -n $1 ]] && printf "%s\n" $1 >&2 } # standard configuration error message to stderr function cfg_err { [[ -n $1 ]] && printf "%s is not set in configuration, but is required by %s to work.\n" $1 $self_name >&2 } # print help function usage { printf "usage: %s [--help|--conf /path/to/config]\n" $self_name printf " --help -h show this message\n" printf " --conf -c use config from the specified path\n\n" printf "Default config path %s will be used if invoked without options\n" $default_cfg } # read the configuration file and spit out some exceptions if stuff is missing function apply_config { # testing remote settings only needed for non-local backups function test_remote_settings { if [[ -z $remote_host ]]; then cfg_err 'remote_host' return 5 fi if [[ -z $remote_user ]]; then cfg_err 'remote_user' return 5 fi if [[ -z $remote_pass ]]; then cfg_err 'remote_pass' return 5 fi if ! (( port )); then err 'remote_port is not a numeric value.' return 5 fi } # import contents of the config (including functions if present) source $cfg || { err "Config file $cfg is unreadable or does not exist"; return 15 } hostname=${local_host:-$HOST} postfix=${outfile_postfix:-$default_postfix} # do the tests if [[ -z $source_dirs ]]; then cfg_err 'source_dirs' return 5 fi # set defaults and / or fail to run if something is missing local exit_code case $protocol in ('ftp'|'ftps') port=${remote_port:-$default_ftp_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;; ('sftp'|'ssh') port=${remote_port:-$default_ssh_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;; ('local') unset remote_port;; (*) cfg_err 'protocol'; return 5;; esac unset exit_code # set variables for tar command case $compress_format in ('xz') compress_flag='J' ;; ('bz2') compress_flag='j' ;; ('gz') compress_flag='z' ;; ('') unset compress_flag ;; (*) err "$compress_format is not a valid value for the compression format option."; return 5;; esac if [[ -n $exclude_list ]]; then if [[ -r $exclude_list ]]; then exclude_option='-X' else err "Exclusion list $exclude_list is either unreadable or does not exist. Proceeding without it." fi fi } # generate the full backup path function generate_fullpath { local backup_type # increment or full backup if [[ -s $snapshot_file ]]; then backup_type='incr' else backup_type='full' fi if [[ -z $backup_filename ]]; then outfile="${backup_dir}${backup_dir:+/}${hostname}-${src_basename}_${postfix}_${backup_type}${gnupg_key:+.gpg.}.t${compress_format:-ar}" else outfile=$backup_filename fi } # compress to stdout function compress { # snapshot file is per directory so we cannot test it within apply_config() if [[ -n $snapshot_file ]]; then if printf '' >> $snapshot_file; then snapshot_option='-g' else err "Snapshot file $snapshot_file cannot be written. Proceeding with full backup." fi fi # do the magic and spit to stdout tar -C $src_basedir -c$compress_flag $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read $src_basename } # store to local or remote function store { # take from stdin and do the magic case $protocol in ('local') dd of=$outfile ;; ('ssh') ssh -p$port $remote_user@$remote_host "dd of=$outfile" ;; ('sftp'|'ftp'|'ftps') curl -ksS -T - $protocol://$remote_host:$port/$outfile -u $remote_user:$remote_pass ;; esac } # encrypt asymmetrically via gpg function encrypt { gpg -r $gnupg_key -e - } # self explanatory, using case statement, so one dash multiple opts is not supported function parse_opts { while [[ -n $1 ]]; do case $1 in ('--help'|'-h') usage; exit 0;; ('--conf'|'-c') shift; opt_cfg=$1; return 0;; (*) err "unknown parameter $1"; exit 127;; esac done } # the main logic function main { # parse options parse_opts $@ # set default config if $opt_cfg is not defined via an option cfg=${opt_cfg:-$default_cfg} # run config tests and fill in defaults apply_config # fail in case something goes wrong local exit_code=$? (( exit_code )) && return $exit_code unset exit_code # run backups per directory for i in $source_dirs; do # prepare the set of variables unset src_basename src_basedir outfile IFS=':' read source_dir snapshot_file <<< $i src_basename=${source_dir:t} src_basedir=${source_dir:h} # generate the backups path generate_fullpath err "Creating a backup of $source_dir via $protocol to store it in $outfile." # pipe magic into magic if [[ -n $gnupg_key ]]; then compress | encrypt | store else compress | store fi done return 0 } main $@