diff --git a/backup.cfg b/backup.cfg new file mode 100644 index 0000000..a676cf2 --- /dev/null +++ b/backup.cfg @@ -0,0 +1,11 @@ +protocol=ssh # ftp, sftp, ftps, ssh or local +remote_host=hostname.tld +backup_dir=relative_or_full_path +#snap_file=/var/backup/snapshot.list +#exclude_list=/usr/local/etc/backup/excludes.list +compress_format=xz # gz, bz2, xz or empty for non-compressed +remote_user=username +remote_pass=PassWd +source_dirs=( '/home/user/source1:/var/backup/snapshot.list' ) + +# vim: ft=zsh diff --git a/backup.zsh b/backup.zsh index 6b13d12..5d7e64a 100755 --- a/backup.zsh +++ b/backup.zsh @@ -1,111 +1,71 @@ #!/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_cfg='/usr/local/etc/backup.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 + [[ -n $1 ]] && echo $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 + [[ -n $1 ]] && echo "$1 is not set in configuration, but is required by $0 to work." >&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 + echo "usage: $self_name [--help|--config] + --help -h - show this message + --config -c - use config from the specified path + + Default config path /usr/local/etc/backup.cfg will be used if invoked without options" } -# read the configuration file and spit out some exceptions if stuff is missing +# function to 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 + source $cfg || err 15 'Config file does not exist' + [[ -z $source_dirs ]] && { cfg_err 'Backup source'; exit 5 } + [[ -z $remote_host ]] && { cfg_err 'Remote host'; exit 5 } + [[ -z $protocol ]] && { cfg_err 'Backup protocol'; exit 5 } + [[ -z $backup_dir && $protocol != 'ssh' ]] && { cfg_err 'Target directory'; exit 5 } + if [[ -z $local_host ]]; then + local_host=$HOST + fi + # date postfix + if [[ -z $outfile_postfix ]]; then + postfix=$default_postfix + else + postfix=$outfile_postfix + fi + if [[ -z $remote_port ]]; then + case $protocol in + ('ftp'|'ftps') remote_port='21';; + ('sftp'|'ssh') remote_port='22';; + ('local') unset remote_port;; + (*) err 1 "$protocol is not a valid value for the protocol option.";; + esac 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;; + ('') unset compress_flag; unset compress_format ;; + (*) err 1 "$compress_format is not a valid value for the compression format option.";; 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." + err 1 "Exclusion list $exclude_list is either unreadable or does not exist." + fi + fi + if [[ -n $snap_file ]]; then + if printf '' >> $snap_file; then + snapshot_option='-g' + else + err 1 "Snapshot file $snap_file cannot be written." fi fi } @@ -115,91 +75,59 @@ function generate_fullpath { local backup_type # increment or full backup - if [[ -s $snapshot_file ]]; then + if [[ -s $snap_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}" + outfile="$backup_dir/${local_host}-${src_basename}_${postfix}_${backup_type}.t${compress_format:-'ar'}" else outfile=$backup_filename fi } -# compress to stdout -function compress +function compress # compress to stdout { - # 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 + tar cf$compress_flag - -C $src_basedir $src_basename $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read } -# store to local or remote -function store +function store # store to local or remote { - # 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 ;; + ('ssh') ssh -p$remote_port $remote_user@$remote_host "dd of=$outfile" ;; + ('sftp'|'ftp'|'ftps') curl -ksS -T - $protocol://$remote_host:$remote_port/$outfile -u $remote_user:$remote_pass ;; + (*) err 1 'Wrong protocol!' ;; 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;; + ('--help'|'-h') usage; return 0;; + ('--config'|'-c') shift; opt_cfg=$1; shift;; 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 + parse_opts + if [[ -z $opt_cfg ]]; then + cfg=$default_cfg + else + cfg=$opt_cfg + fi 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 + IFS=':' read source_dir snap_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 + compress | store done return 0 } diff --git a/backup.zsh.cfg b/backup.zsh.cfg deleted file mode 100644 index b773677..0000000 --- a/backup.zsh.cfg +++ /dev/null @@ -1,56 +0,0 @@ -#### general options #### -## The protocol we want to use to store our backups. -## Can be ftp, sftp, ftps, ssh or local. -protocol='ssh' - -## The directory to store backups in, locally or remotely. -backup_dir='relative_or_full_path' - -## The list of patterns to exclude from backups, for -## more details look into tar -X option. -#exclude_list='/usr/local/etc/backup/excludes.list' - -## The compression algorithm for backups. -## Can be gz, bz2, xz or empty (for non-compressed). -compress_format='xz' - -## An array with the set of directories within it. -## Optionally snapshot file can be added to store -## incremental diffs (tar -g option used). -## You'll have to deal with snapshots on your own: -## backup.zsh only handles backups (i.e. you can remove -## snapshot via cron on regular basis to ensure that -## full backups are created from time to time.) -source_dirs=( '/home/user/source1:/var/backup/snapshot.list' - '/etc' '/var/spool/mail:/var/backup/spool_snapshot.list' ) - -## Use with caution, the file existance is not checked -## on execution. -## Since this config is sourced, I advise adding some -## logic for that, or you can handle filename collisions -## externally. -#backup_filename='somebackup' - -## GPG key to encrypt backups, uses name of the private -## key in your keyring. It is also entirely possible to -## add the GNUPGHOME environment variable export here in -## order to use the private key from a specific location. -#gnupg_key='keyname' - -#### remote options #### -## Remote host. -remote_host='hostname.tld' - -## Remote user. -remote_user='username' - -## Password, due to how openssh handles security it only -## works for *ftp* protocols; backups via ssh protocol -## work interactively. Later versions will have support -## for ssh keys... If I ever decide to make it happen. -remote_pass='PassWd' - -## Port is optional, the defaults are hardcoded. -#remote_port='443' - -# vim: ft=zsh