A simple backup script nobody asked for.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

backup.zsh 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env zsh
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) 2014 Von Random
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in all
  15. # copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. # SOFTWARE.
  24. #
  25. self_name=$0
  26. # some hardcoded defaults
  27. default_cfg='/etc/backup.zsh.cfg'
  28. default_postfix=$(date +%F-%H%M)
  29. default_ftp_port='21'
  30. default_ssh_port='22'
  31. # echo to stderr
  32. function err
  33. {
  34. [[ -n $1 ]] && printf "%s\n" $1 >&2
  35. }
  36. # standard configuration error message to stderr
  37. function cfg_err
  38. {
  39. [[ -n $1 ]] && printf "%s is not set in configuration, but is required by %s to work.\n" $1 $self_name >&2
  40. }
  41. # print help
  42. function usage
  43. {
  44. printf "usage: %s [--help|--conf /path/to/config]\n" $self_name
  45. printf " --help -h show this message\n"
  46. printf " --conf -c use config from the specified path\n\n"
  47. printf "Default config path %s will be used if invoked without options\n" $default_cfg
  48. }
  49. # read the configuration file and spit out some exceptions if stuff is missing
  50. function apply_config
  51. {
  52. # testing remote settings only needed for non-local backups
  53. function test_remote_settings
  54. {
  55. if [[ -z $remote_host ]]; then
  56. cfg_err 'remote_host'
  57. return 5
  58. fi
  59. if [[ -z $remote_user ]]; then
  60. cfg_err 'remote_user'
  61. return 5
  62. fi
  63. if [[ -z $remote_pass ]]; then
  64. cfg_err 'remote_pass'
  65. return 5
  66. fi
  67. if ! (( port )); then
  68. err 'remote_port is not a numeric value.'
  69. return 5
  70. fi
  71. }
  72. # import contents of the config (including functions if present)
  73. source $cfg || { err "Config file $cfg is unreadable or does not exist"; return 15 }
  74. hostname=${local_host:-$HOST}
  75. postfix=${outfile_postfix:-$default_postfix}
  76. # do the tests
  77. if [[ -z $source_dirs ]]; then
  78. cfg_err 'source_dirs'
  79. return 5
  80. fi
  81. # set defaults and / or fail to run if something is missing
  82. local exit_code
  83. case $protocol in
  84. ('ftp'|'ftps') port=${remote_port:-$default_ftp_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;;
  85. ('sftp'|'ssh') port=${remote_port:-$default_ssh_port}; test_remote_settings; exit_code=$?; (( exit_code )) && return $exit_code;;
  86. ('local') unset remote_port;;
  87. (*) cfg_err 'protocol'; return 5;;
  88. esac
  89. unset exit_code
  90. # set variables for tar command
  91. case $compress_format in
  92. ('xz') compress_flag='J' ;;
  93. ('bz2') compress_flag='j' ;;
  94. ('gz') compress_flag='z' ;;
  95. ('') unset compress_flag ;;
  96. (*) err "$compress_format is not a valid value for the compression format option."; return 5;;
  97. esac
  98. if [[ -n $exclude_list ]]; then
  99. if [[ -r $exclude_list ]]; then
  100. exclude_option='-X'
  101. else
  102. err "Exclusion list $exclude_list is either unreadable or does not exist. Proceeding without it."
  103. fi
  104. fi
  105. }
  106. # generate the full backup path
  107. function generate_fullpath
  108. {
  109. local backup_type
  110. # increment or full backup
  111. if [[ -s $snapshot_file ]]; then
  112. backup_type='incr'
  113. else
  114. backup_type='full'
  115. fi
  116. if [[ -z $backup_filename ]]; then
  117. outfile="${backup_dir}${backup_dir:+/}${hostname}-${src_basename}_${postfix}_${backup_type}${gnupg_key:+.gpg.}.t${compress_format:-ar}"
  118. else
  119. outfile=$backup_filename
  120. fi
  121. }
  122. # compress to stdout
  123. function compress
  124. {
  125. # snapshot file is per directory so we cannot test it within apply_config()
  126. if [[ -n $snapshot_file ]]; then
  127. if printf '' >> $snapshot_file; then
  128. snapshot_option='-g'
  129. else
  130. err "Snapshot file $snapshot_file cannot be written. Proceeding with full backup."
  131. fi
  132. fi
  133. # do the magic and spit to stdout
  134. tar -C $src_basedir -c$compress_flag $snapshot_option $snapshot_file $exclude_option $exclude_list --ignore-failed-read $src_basename
  135. }
  136. # store to local or remote
  137. function store
  138. {
  139. # take from stdin and do the magic
  140. case $protocol in
  141. ('local') dd of=$outfile ;;
  142. ('ssh') ssh -p$port $remote_user@$remote_host "dd of=$outfile" ;;
  143. ('sftp'|'ftp'|'ftps') curl -ksS -T - $protocol://$remote_host:$port/$outfile -u $remote_user:$remote_pass ;;
  144. esac
  145. }
  146. # encrypt asymmetrically via gpg
  147. function encrypt
  148. {
  149. gpg -r $gnupg_key -e -
  150. }
  151. # self explanatory, using case statement, so one dash multiple opts is not supported
  152. function parse_opts
  153. {
  154. while [[ -n $1 ]]; do
  155. case $1 in
  156. ('--help'|'-h') usage; exit 0;;
  157. ('--conf'|'-c') shift; opt_cfg=$1; return 0;;
  158. (*) err "unknown parameter $1"; exit 127;;
  159. esac
  160. done
  161. }
  162. # the main logic
  163. function main
  164. {
  165. # parse options
  166. parse_opts $@
  167. # set default config if $opt_cfg is not defined via an option
  168. cfg=${opt_cfg:-$default_cfg}
  169. # run config tests and fill in defaults
  170. apply_config
  171. # fail in case something goes wrong
  172. local exit_code=$?
  173. (( exit_code )) && return $exit_code
  174. unset exit_code
  175. # run backups per directory
  176. for i in $source_dirs; do
  177. # prepare the set of variables
  178. unset src_basename src_basedir outfile
  179. IFS=':' read source_dir snapshot_file <<< $i
  180. src_basename=${source_dir:t}
  181. src_basedir=${source_dir:h}
  182. # generate the backups path
  183. generate_fullpath
  184. err "Creating a backup of $source_dir via $protocol to store it in $outfile."
  185. # pipe magic into magic
  186. if [[ -n $gnupg_key ]]; then
  187. compress | encrypt | store
  188. else
  189. compress | store
  190. fi
  191. done
  192. return 0
  193. }
  194. main $@