Compare commits
10 commits
31ba06126a
...
a681b52517
Author | SHA1 | Date | |
---|---|---|---|
|
a681b52517 | ||
6f2158be7c | |||
f515160bd6 | |||
d7ec2ce02f | |||
b5d0158936 | |||
175c7979f9 | |||
4eaaf32b10 | |||
|
ec4a6119f7 | ||
|
0aabfe44bf | ||
|
ffe5c9ac16 |
3 changed files with 188 additions and 71 deletions
11
backup.cfg
11
backup.cfg
|
@ -1,11 +0,0 @@
|
|||
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
|
192
backup.zsh
192
backup.zsh
|
@ -1,71 +1,111 @@
|
|||
#!/usr/bin/env zsh
|
||||
self_name=$0
|
||||
default_cfg='/usr/local/etc/backup.cfg'
|
||||
default_postfix=$(date +%F-%H%M)
|
||||
#
|
||||
# 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 ]] && echo $1 >&2
|
||||
[[ -n $1 ]] && printf "%s\n" $1 >&2
|
||||
}
|
||||
|
||||
# standard configuration error message to stderr
|
||||
function cfg_err
|
||||
{
|
||||
[[ -n $1 ]] && echo "$1 is not set in configuration, but is required by $0 to work." >&2
|
||||
[[ -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
|
||||
{
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
# function to read the configuration file and spit out some exceptions if stuff is missing
|
||||
# read the configuration file and spit out some exceptions if stuff is missing
|
||||
function apply_config
|
||||
{
|
||||
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
|
||||
# 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; unset compress_format ;;
|
||||
(*) err 1 "$compress_format is not a valid value for the compression format option.";;
|
||||
('') 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 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."
|
||||
err "Exclusion list $exclude_list is either unreadable or does not exist. Proceeding without it."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
@ -75,59 +115,91 @@ function generate_fullpath
|
|||
{
|
||||
local backup_type
|
||||
# increment or full backup
|
||||
if [[ -s $snap_file ]]; then
|
||||
if [[ -s $snapshot_file ]]; then
|
||||
backup_type='incr'
|
||||
else
|
||||
backup_type='full'
|
||||
fi
|
||||
if [[ -z $backup_filename ]]; then
|
||||
outfile="$backup_dir/${local_host}-${src_basename}_${postfix}_${backup_type}.t${compress_format:-'ar'}"
|
||||
outfile="${backup_dir}${backup_dir:+/}${hostname}-${src_basename}_${postfix}_${backup_type}${gnupg_key:+.gpg.}.t${compress_format:-ar}"
|
||||
else
|
||||
outfile=$backup_filename
|
||||
fi
|
||||
}
|
||||
|
||||
function compress # compress to stdout
|
||||
# compress to stdout
|
||||
function compress
|
||||
{
|
||||
tar cf$compress_flag - -C $src_basedir $src_basename $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read
|
||||
# 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
|
||||
}
|
||||
|
||||
function store # store to local or remote
|
||||
# store to local or remote
|
||||
function store
|
||||
{
|
||||
# take from stdin and do the magic
|
||||
case $protocol in
|
||||
('local') dd of=$outfile ;;
|
||||
('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!' ;;
|
||||
('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; return 0;;
|
||||
('--config'|'-c') shift; opt_cfg=$1; shift;;
|
||||
('--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_opts
|
||||
if [[ -z $opt_cfg ]]; then
|
||||
cfg=$default_cfg
|
||||
else
|
||||
cfg=$opt_cfg
|
||||
fi
|
||||
# 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 snap_file <<< $i
|
||||
IFS=':' read source_dir snapshot_file <<< $i
|
||||
src_basename=${source_dir:t}
|
||||
src_basedir=${source_dir:h}
|
||||
# generate the backups path
|
||||
generate_fullpath
|
||||
compress | store
|
||||
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
|
||||
}
|
||||
|
|
56
backup.zsh.cfg
Normal file
56
backup.zsh.cfg
Normal file
|
@ -0,0 +1,56 @@
|
|||
#### 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
|
Loading…
Reference in a new issue