diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..feff5fa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# baka 0.2.0 + +WARNING! All changes in this version breaks backward capatibility! + +# CLI + +New options: + +- `--config`. Specify your baka.conf file. +- `backup --dry-run`. Test configuration without backup. +- `remove --list`. Print files to delete. + +Changed: + +- `list` command now has two options `--verbose` and `--help`. Verbose output show Entry name, path and local and remote storages in table view. + +Removed: + +- `show` command. View configuration file directly instead. +- `edit` command. Edit configuration file directly instead. +- `test` command. Use `backup --dry-run` instead. + +# baka.conf (former name: main.conf) + +Added: + +- Default parameter values now is built in baka. Override it in baka.conf +- `entries` variable. You can set entries directory in baka.conf +- `filename_format`. Special formatting for filenames. +- `autoprefix`. Add automatic prefix to filenames (based on entry file name). +- `dir_per_date`. Backups path format. See baka(1). + +Changed: + +- `dest` now is `local` and can be used in baka.conf and entries. +- `remote` now is URI (like `[scheme]://[user[:password]]@[host[:port]][/path]`) and can be used in baka.conf and entries. + +Removed: + +- `nf`, use `filename_format` instead +- `df`, use `filename_format` +- `log_df`, use `log_format` istead + +# Entries + +Added: + +- `local`. This is renamed `dest`. +- `remote`. Now you can specify remote storage per entry. +- `prefix`. Override filename prefix. +- `archive`. Renamed `files`. Archive files into **.tar.gz**. +- `copy`. Simple file copy without compression. +- `database`. Replacement for `mysql` and `postrges` variables. Provide DB URI. + +Removed: + +- `dest`. Use `local` instead. +- `mysql` and `postgres` variables. Use `database` instead. +- `files` variable. Use `archve` instead. + +# baka 0.1.2 + +Initial release. diff --git a/README.md b/README.md index 67240d3..c0ed54e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # baka -Simple and flexible file and database backup software written in Bash. +Simple and flexible full backup software written in Bash. More info in manuals: @@ -13,13 +13,13 @@ More info in manuals: Add repository to /etc/apt/sources.list.d/: ``` -sudo echo 'deb [arch = all] http://repos.gch.icu/debian testing main' > /etc/apt/sources.list.d/gd-debian.list +sudo echo 'deb [arch=all] http://repos.gch.icu/debian testing main' > /etc/apt/sources.list.d/ge.list ``` Add key: ``` -curl -s http://repos.gch.icu/DEB-GPG-KEY | sudo gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/gd-debian.gpg --import +curl -s http://repos.gch.icu/DEB-GPG-KEY | sudo gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/ge.gpg --import ``` Update package list and install baka: diff --git a/baka b/baka deleted file mode 100755 index 2b005a1..0000000 --- a/baka +++ /dev/null @@ -1,1199 +0,0 @@ -#!/usr/bin/env bash - -########################################################################## -# -# baka -# -# Do files and databases backup. -# -# -# COPYING -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# 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, see . -# -########################################################################## - -set -o errexit - -baka_version=0.1.2 - -main_conf=/etc/baka/main.conf # Default: /etc/baka/main.conf -baka_entries=/etc/baka/entries # Default: /etc/baka/entries - -bk_print_help() { -cat << EOF -Do files and databases backup. - -Usage: baka [--version] [--help | help] [...] - -Commands and options: - backup do backup right now. - list list backup entries. - test test main configuration and all of entries. - show print main configuration to STDOUT. - edit edit main configuration in default editor.. - remove remove old backups. - --version print version and exit. - --help, help print this help message and exit. - -See 'man baka' or 'baka help' for more info. - - , -(#`皿´) - -Senpai wants you to do backups! - -EOF -exit 0 -} - -bk_print_version() { - echo "baka v$baka_version" - exit 0 -} - -bk_getopts() { - # GNU-style CLI options parser. - # - # Parse --opt VAL and --opt=VAL options. - # Requires 2 arguments: $1, $2. - # Returns: - # $opt - option name. - # $arg - option's value. - # $sft - value for shift. - - if [[ "$1" =~ .+=.+ ]]; then - opt="${1%%=*}"; arg="${1#*=}"; sft=1 - elif [[ ! "$1" =~ .+=$ ]] && \ - [ "$2" ] && [ "${2:0:1}" != '-' ] - then - opt="$1"; arg="$2"; sft=2 - else - opt="$1" - if [[ "$1" =~ .+=$ ]]; then opt="${1:0: -1}"; fi - bk_err -s "missing argument for: $opt" - fi -} - -bk_err() { - # Show error message and write it into log. - # $1 is error message. - - while (( "$#" )) - do - case "$1" in - -s) # Don't write error log. - local only_stderr=1; shift;; - *) local err_message="$1" ; shift;; - esac - done - - local progname='baka' - echo -e "$progname: $err_message" | sed 's/^ *//g' >&2 - [ "$only_stderr" ] || bk_log -n "Error: $err_message" - exit 1 -} - -bk_error_report() { - # Error reporting to email. - # $1 is error message. - # $mail_to must be set in main.conf. - # This function is NOT USED. - # TODO - - err_log_start="$( \ - grep -n '\[Started\]' "$log" | tail -1 | cut -d ':' -f 1 \ - )" # Get log start line number. - - error_log="$(tail -n +"$err_log_start" "$log")" - - echo -e \ - "Error report $(date +"$log_df")\n\nMessage:\n$1\n\nLog:\n\n$error_log" \ - | mail -s "baka: Backup error: $HOSTNAME" "$mail_to" -} - -bk_log() { - # Logging. - - [[ "$#" > 2 ]] && echo "bk_log: Too many args." >&2 - while (( "$#" )) - do - case "$1" in - -n) local no_print=1; shift;; - *) local log_message="$1"; shift;; - esac - done - - # If log message not sent as argument read stdin. - if [[ ! "$log_message" ]] - then - local log_message="$(cat <&0)" - fi - - # Don't write log if $log_message is empty. - [[ "$log_message" == '' ]] && return 0 - - # $1 is log message (can be multistring). - local escaped_log_message="$( \ - printf '%s\n' "$log_message" | sed 's/[\/&]/\\&/g' \ - )" # Escape log message. - - log_formatter() { - # $1 - format string. - # $2 - log string. - sed -e \ - "s/%time/$(date +"$log_df")/g;s/%log/$2/g" <<< "$1" - } - - while IFS= read -r log_line - do - if [ ! "$no_print" ] - then - echo -e "$log_message" - fi - log_formatter "$log_format" "$log_line" >> "$log" - done <<< "$escaped_log_message" -} - -bk_yn_dialog() { - local question="$1" # Message prompt. - local yes=0 - local no=1 - local pending=2 - - [ "$assume_yes" = "1" ] && return "$yes" - - local answer=$pending - - while [ $answer -eq $pending ] - do - echo -en "$question [y/n] " - read -r reply - case "$reply" in - y|Y|Yes|YES) answer=$yes;; - n|N|No|NO) answer=$no;; - *) echo 'Please, answer y or n';; - esac - done - - return "$answer" -} - -bk_check_rsync() { - if hash rsync 2>/dev/null - then - rsync_bin=/usr/bin/rsync - else - bk_err -s 'rsync executable not found.' - fi -} - -bk_check_s3cmd() { - if hash s3cmd 2>/dev/null - then - # If s3cmd is installed via OS package manager. - s3cmd_bin=/usr/bin/s3cmd - elif [ -f /usr/local/bin/s3cmd ] - then - # If s3cmd is installed globally via Python pip. - s3cmd_bin=/usr/local/bin/s3cmd - else - bk_err -s 's3cmd executable not found.' - fi -} - -# CONFIG PARSER -########################################################################## - -bk_read_config() { - # Read file passed as $1 and parse lines. Return clean config. - # - # - Remove spaces and tabs across '=' - # - Remove comments ('#') - # - Remove blank lines - # - Escape whitespaces and special characters - local bk_config="$(sed \ - 's/[[:space:]]\+=[[:space:]]\+/=/g;/^#/d;/^$/d;s/#.*//g' <(\ - cat "$1" | grep = \ - ) | while read -r LN; do echo $(printf '%q' "$LN"); done)" - echo "$bk_config" -} - -bk_parse_main_conf() { - # Main baka config parser. - if ! [ -f "$main_conf" ] - then - bk_err -s "$main_conf: file not found." - fi - - eval "$(bk_read_config "$main_conf")" - - # Check the minimal set of parameters. - if [[ \ - "$log" && \ - "$log_df" && \ - "$log_format" && \ - "$df" && \ - "$nf" && \ - "$remote" && \ - "$livetime" \ - ]] - then - : # Do nothing if all set. - else - bk_err -s \ - "Some required parameters is not set. Check $main_conf - See 'man baka' for more info." - fi - - # Check remote storage settings. - if [ "$remote" = 'none' ] # Do nothing if set to 'none'. - then - : # Do nothing. - elif [ "$remote" = 'rsync' ] # Check rsync and optional params. - then - bk_check_rsync - if [ ! "$ssh_uri" ] - then - bk_err -s "You set rsync for copy to remote storage, but - ssh_uri is not set. Please, check $main_conf" - fi - elif [ "$remote" = 's3' ] # Check s3cmd and optional params. - then - bk_check_s3cmd - if [ ! "$s3_uri" ] - then - bk_err -s "You set s3 for copy to remote storage, but - s3_uri is not set. Please, check $main_conf" - fi - else # Exit if 'remote' has another value. - bk_err -s "$main_conf: $remote: bad value. See 'man baka' for info." - fi -} - -bk_find_entries() { - # Return array '$bk_all_entries'with files - # from /etc/baka/entries ($baka_entries). Resolve symlinks too. - local all_files="$(find "$baka_entries" -type f)" - - for file in $all_files - do - # Resolve symlinks. - if [ -L "$file" ]; then - s="$(readlink "$file")" - if [ -f "$s" ]; then - file="$s" - fi - fi - # Collect all entries, except ignored if set. - if [[ ! "${ignore[@]}" =~ "$(basename $file)" ]] - then - bk_all_entries+=("$file") - fi - done - - # Exit if nothing found. - if [[ "${#bk_all_entries[@]}" == 0 ]] - then - bk_err "$baka_entries: no entries to backup." - fi - - # SECURITY ISSUE FIX. - # - # Check entries and main.conf permissions. - for item in "$bk_all_entries" "$main_conf" - do - if [[ "$(stat -c "%a" "$item")" > 644 ]] - then - bk_err "$item: permissions are too open - It is recommended that your configurations are NOT accessible by others." - else - : - fi - done -} - -bk_selected_entries() { - for entry in ${entries[@]} - do - entry="$baka_entries"/"$entry" - if [ -f "$entry" ] - then - bk_all_entries+=("$entry") - else - bk_err "$entry: file not found." - fi - done -} - -bk_parse_baka_entry() { - # Parse single entry file from $baka_entries dir. - # $1 is entry filename. - local bk_entry="$(bk_read_config "$1")" - - if [ "$local" ]; then - dest_dir="$local" - else - dest_dir="$( \ - awk -F '=' '/dest/ {print $2}' <<< "$bk_entry" \ - )" - # Check the dest parameter. - if [[ $(grep -c dest <<< "$bk_entry") == 0 ]] - then - bk_err "$1: 'dest' parameter is not set." - elif [[ $(grep -c dest <<< "$bk_entry") > 1 ]] - then - bk_err "$1: double 'dest' parameter." - elif [ ! -d "$dest_dir" ] - then - bk_err "$1: $dest_dir: destination dir does not exist" - fi - fi - - # Check other parameters. - if [[ ! $( \ - egrep "files|postgres|mysql|command" <<< "$bk_entry" \ - ) ]] - then - bk_err "$1: no data to backup. - Please set files, databases or commands in $1. - See 'man baka' for more info." - fi -} - -bk_get_entry_data() { - # Returns array of entry parameter values. - # For example you wrote 3 different database requisites - # in /etc/baka/entries/example.conf: - # mysql = db1:user1:pass1 - # mysql = db2:user2:pass2 - # mysql = db3:user3:pass3 - # You receive array: - # bk_mysql = ( db1:user1:pass1 db2:user2:pass2 db3:user3:pass3 ) - # - # Parameters for this function: - # $1 is entry file (path). - # $2 is search query (e.g.: mysql, postgres, files, etc.). - - eval "bk_$2=()" # Set empty data to prevent dublicates. - - # Parse file. - entry_data="$(bk_read_config "$1")" - - # Collect values. - local raw_values="$( - grep "$2" <<< "$entry_data" \ - | awk -F '=' '{for(i=2;i<=NF;i++){printf "%s ", $i}}' \ - )" - - for value in "$raw_values" - do - eval "bk_$2+=("$value")" - done -} - -bk_parse_nf() { - # Parse name format. - # Refference: - # %type - # 'backup' for files or 'dump' for databases. - # %name - # directory or database name. - # %time - # datetime formatted by 'df' option. - # Example: - # nf = '%type_%name_%time' - - local type="$1" - local name="$(basename "$2")" - - sed "s/%type/$type/g; - s/%name/$name/g; - s/%time/$(date +"$df")/g" <<< "$nf" -} - -bk_parse_db_reqs() { - # Database requisites parser. - # $1 is string with semicolon separated requisites. - - # Characters : (semicolon) and # (hash) - # is not allowed in database passwords! - # Password will be broken after parsing. - - local entry_count=$(awk -F: '{print NF}' <<< "$1") - - if [ "$entry_count" -eq 5 ]; then - db_host=$(cut -d ':' -f 1 <<< "$1") - db_port=$(cut -d ':' -f 2 <<< "$1") - db_name=$(cut -d ':' -f 3 <<< "$1") - db_user=$(cut -d ':' -f 4 <<< "$1") - db_pass=$(cut -d ':' -f 5 <<< "$1") - elif [ "$entry_count" -eq 4 ]; then - db_host=$(cut -d ':' -f 1 <<< "$1") - # db_port will be set later. - db_name=$(cut -d ':' -f 2 <<< "$1") - db_user=$(cut -d ':' -f 3 <<< "$1") - db_pass=$(cut -d ':' -f 4 <<< "$1") - elif [ "$entry_count" -eq 3 ]; then - db_host=localhost - # db_port will be set later. - db_name=$(cut -d ':' -f 1 <<< "$1") - db_user=$(cut -d ':' -f 2 <<< "$1") - db_pass=$(cut -d ':' -f 3 <<< "$1") - else - bk_err "$1: bad database requisites. - Correct syntax is host:port:db:user:pass. See docs for more info." - fi -} - -bk_show_db_reqs() { - # $1 is password show/hide flag. - echo "Host: $db_host" - echo "Port: $db_port" - echo "Database: $db_name" - echo "User: $db_user" - if [ "$1" ] - then - echo "Password: $db_pass" - else - echo "Password: hidden" - fi - echo "---" -} - -# BACKUP -########################################################################## - -bk_do_backup_help() { -cat << EOF -Do backup. - -Usage: baka backup [--help | help] [-i | --ignore=] - [-e | --entry=] [--local] [--no-verify] - [--remove] -Options: - -i, --ignore= run backup for all entries, except ignored. - -e, --entry= run backup for selected entry. - --local force local backup. - --no-verify don't check archives integrity. - --remove remove old backups (forced). - --help, help print this message and exit. -EOF -exit 0 -} - -bk_backup_files() { - - bk_exclude_items() { - # Exclude files and directories from backup. - for items in ${bk_exclude[*]} - do - items="$( tr ',' ' ' <<< "$items")" - for item in $items; do - echo -en "--exclude='$item' " - done - done - } - - for filepath in "${bk_files[@]}" - do - if [ -f "$filepath" ] || [ -d "$filepath" ] - then - : - else - bk_err "$filepath: no such file or directory." - fi - - echo "Archiving files: $filepath ..." | bk_log - - # Archive name. $filepath is path set in /etc/baka/entries/* in 'files'. - dest_file="$dest_dir"/"$(bk_parse_nf backup "$filepath")".tar.gz - - # Do archive! - eval \ - tar -czf "$dest_file" $(bk_exclude_items) \ - -C $(dirname "$filepath") $(basename "$filepath") \ - |& bk_log -n - - bk_log "Archiving files: $filepath [Done]" - - # Verifying. - if [ ! "$no_verify" ]; then - if gunzip -c "$dest_file" | tar -t > /dev/null - then - bk_log "Integrity (gzip uncompressing and tar -t) [Success]" - else - bk_log "Integrity (gzip uncompressing and tar -t) [Fail]" - fi - - local in_dir="$(find "$filepath" | wc -l)" - local in_tar="$(tar -tf "$dest_file" | wc -l)" - - if [[ "$in_dir" == "$in_tar" ]] - then - bk_log \ - "Completeness: items: $in_dir, archived: $in_tar [Success]" - elif [[ "${#bk_exclude[@]}" != 0 ]] - then - echo "Completeness: Some files is excluded by configuration: - items: $in_dir, archived: $in_tar [Skipped]" \ - | sed 's/^ *//g' | tr '\n' ' ' | bk_log - else - bk_err \ - "Completeness: items: $in_dir, archived: $in_tar [Failed]" - fi - else - : # Don't verify. - fi - - bk_log "Archive saved as: $dest_file" - bk_upload_file "$dest_file" - done -} - -bk_run_command() { - # Run commands listed in entry. Disabled by default. - # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - # IT'S DANGEROUS! DON'T USE UNTRUSTED COMMANDS IN ENTRY! - # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - if [[ "$allow_commands" == "true" ]] - then - for command in "${bk_command[@]}" - do - bk_log "Executing command: $command" - # Run! - eval "$(printf "$command")" - done - else - bk_log "Error: tried to execute commands: ${bk_command[@]}" - bk_err "Commands executing is not allowed!" - fi -} - -bk_backup_mysql() { - # Do MariaDB/MySQL dump. - - for reqs in "${bk_mysql[@]}" - do - bk_parse_db_reqs "$reqs" - - bk_log "Dumping database: $db_name@$db_host (MySQL) ..." - - dump_name="$dest_dir"/"$(bk_parse_nf dump "$db_name")".sql.gz - [ "$db_port" ] || db_port=3306 # Set default MySQL port. - - mysqldump \ - --host="$db_host" \ - --port="$db_port" \ - "$db_name" \ - --user="$db_user" \ - --password="$db_pass" \ - | gzip -c > "$dump_name" |& bk_log - - if [ -s "$dump_name" ]; then - bk_log "Dumping database: $db_name@$db_host (MySQL) [Done]" - bk_log "Dump saved as: $dump_name" - bk_upload_file "$dump_name" - else - rm "$dump_name" - bk_err "Something went wrong. Dump size is 0 bytes. Removing $dump_name" - fi - done -} - -bk_backup_postgres() { - # Do PostgreSQL dump. - - for reqs in "${bk_postgres[@]}" - do - bk_parse_db_reqs "$reqs" - - bk_log "Dumping database: $db_name@$db_host (PostgreSQL) ..." - - dump_name="$dest_dir"/"$(bk_parse_nf dump "$db_name")".psql.gz - [ "$db_port" ] || db_port=5432 # Set default PostgreSQL port. - export PGPASSWORD="$db_pass" - - pg_dump \ - --host="$db_host" \ - --port="$db_port" \ - --dbname="$db_name" \ - --username="$db_user" \ - --no-password | gzip -c > "$dump_name" |& bk_log - - unset PGPASSWORD - - if [ -s "$dump_name" ]; then - bk_log "Dumping database: $db_name@$db_host (PostgreSQL) [Done]" - bk_log "Dump saved as: $dump_name" - bk_upload_file "$dump_name" - else - rm "$dump_name" - bk_err "Something went wrong. Dump size is 0 bytes. Removing $dump_name" - fi - done -} - -bk_upload_via_rsync() { - # RSYNC - local filename="$1" - [ "$ssh_port" ] || ssh_port=22 - rsync -a -e "ssh -p $ssh_port" "$filename" "$ssh_uri" | bk_log -} - -bk_upload_via_s3cmd() { - # S3 - local filename="$1" - s3cmd -pq --no-progress put "$filename" "$s3_uri" | bk_log -} - -bk_upload_file() { - # Select function to upload. - # - # $1 -- file passed to upload. - - if [ "$remote" == 'none' ]; then - : - else - bk_log "Uploading ${1##*/} to remote server ..." - case "$remote" in - rsync) bk_upload_via_rsync "$1";; - s3) bk_upload_via_s3cmd "$1";; - esac - bk_log "Uploading ${1##*/} [Done]" - fi -} - -bk_do_backup() { - # Main function for 'backup' command. - - # Parse args. - if [[ "$@" =~ -i|--i ]] && [[ "$@" =~ -e|--e ]] - then - bk_err \ - "You can not use '--ignore' and '--entry' options together. - See 'baka backup help' or 'man baka' for more info." - fi - - while (( "$#" )) - do - case "$1" in - -i|--ignore|--ignore=*) - bk_getopts "$1" "$2" - ignore+=("$arg") - shift "$sft";; - -e|--entry|--entry=*) - bk_getopts "$1" "$2" - entries+=("$arg") - shift "$sft";; - --no-verify) no_verify=1; shift;; - --local) no_remote=1; shift;; - --remove) remove_backups=1; shift;; - --help|help) - bk_do_backup_help;; - *) bk_err -s "bad option: $1";; - esac - done - - # Startup log. - bk_log -n "Backup [Started]" - echo "Backup: $(date +"$log_df")" - bk_log \ - "Destination dir (local): $([ $local ] && echo $local || echo 'from entry')" - - # Force local backup ('--local' option). - if [ "$no_remote" ]; then - remote='none' - bk_log \ - "Local backup forced! Files does not be uploaded to remote storage." - fi - - bk_log "Remote transport: $remote" - case "$remote" in - none) remote_uri='none';; - s3) remote_uri=$s3_uri;; - rsync) remote_uri=$ssh_uri;; - esac - bk_log "Remote URI: $remote_uri" - - # Start entries processing. - if [ "$entries" ] # --entry option. - then - bk_selected_entries - # Selected entries basenames. - # Used only for display in echo below. - local seb="$( \ - for ent in ${bk_all_entries[@]} - do echo -n "$(basename $ent) "; done - )" - echo "Selected entries: $seb" | bk_log - else - [ "$ignore" ] && \ - echo "Skipped entries: ${ignore[@]}" | bk_log - bk_find_entries - fi - - # Non printing log. - echo \ - "Entries to backup (${#bk_all_entries[@]}): ${bk_all_entries[@]}" | bk_log -n - - # Entry counter. - ecnt=${#bk_all_entries[@]} # Entries count. - iter=1 # Iterator. - - # For every entry in $baka_entries ... - for entry in "${!bk_all_entries[@]}" - do - # Display entry name and counter. - # Output: -> Entry: example.org [1/5] - echo - bk_log \ - "-> Entry: ${bk_all_entries[iter-1]##*/} [$(($entry+1))/$ecnt]" - - bk_parse_baka_entry "${bk_all_entries[iter-1]}" - - # Get data. - # 'dest' is already parsed in bk_parse_baka_entry(). - bk_get_entry_data "${bk_all_entries[iter-1]}" 'files' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'exclude' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'command' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'mysql' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'postgres' - - bk_log "Destination dir: $dest_dir" - - # Do backups! - if [[ "${#bk_command[@]}" != 0 ]]; then - bk_run_command - fi - if [[ "${#bk_files[@]}" != 0 ]]; then - bk_backup_files - fi - if [[ "${#bk_mysql[@]}" != 0 ]]; then - bk_backup_mysql - fi - if [[ "${#bk_postgres[@]}" != 0 ]]; then - bk_backup_postgres - fi - - let iter++ # Increase iterator for entry counter. - done - - # Remove old backups ('--remove' option). - if [ "$remove_backups" ]; then - echo - echo "-> Remove old backups ..." - bk_get_dest_dirs # Get $dest_dirs - for dir in "${dest_dirs[@]}" - do - bk_delete_files "$dir" - done - fi - - echo # Just print new line. - bk_log "Backup [Finished]" -} - -# OTHER OPTIONS -########################################################################## - -# BAKA LIST -# Show entries list. -###################### - -bk_show_list_help() { -cat << EOF -Show entries list. - -Usage: baka list [--help | help] [-v| --verbose] [-s| --short] [-S] - -Options: - -v, --verbose print entries list verbosely (default). - -s, --short short format (names). - -S short format (pathes). - --help, help print this message and exit. -EOF -exit 0 -} - -bk_show_list() { - - verbose_list=2 # Default view mode. - - while (( "$#" )) - do - case "$1" in - -v|--verbose) verbose_list=2; shift;; - -s|--short) verbose_list=1; shift;; - -S) verbose_list=0; shift;; - --help|help) - bk_show_list_help;; - *) bk_err -s "bad option: $1";; - esac - done - - bk_find_entries # Get entries list. - - if [ "$verbose_list" -eq 2 ]; then - # Verbosely. - for entry in "${bk_all_entries[@]}" - do - echo -e "${entry##*/}\t($entry)" - done - elif [ "$verbose_list" -eq 1 ]; then - # Short format (names). - for entry in "${bk_all_entries[@]}" - do - echo "${entry##*/}" - done - elif [ "$verbose_list" -eq 0 ]; then - # Short format (pathes). - for entry in "${bk_all_entries[@]}" - do - echo "$entry" - done - fi -} - -# BAKA TEST -# Test all entries. -##################### - -bk_do_test_help() { -cat << EOF -Do tests. - -Usage: baka test [--help | help] [-v| --verbose] - -Options: - -v, --verbose print output verbosely. - --help, help print this message and exit. -EOF -exit 0 -} - -bk_do_test() { - - while (( "$#" )) - do - case "$1" in - -v|--verbose) verbose_test=1; shift;; - --help|help) - bk_do_test_help;; - *) bk_err -s "bad option: $1";; - esac - done - - bk_find_entries # Get entries list. - - echo # Just print new line. - - # Entry counter. - ecnt=${#bk_all_entries[@]} # Entries count. - iter=1 # Iterator. - - for entry in "${!bk_all_entries[@]}" - do - - # Display entry name and counter. - # Output: -> Entry: example.org [1/2] - echo \ - "-> Entry: ${bk_all_entries[iter-1]##*/} [$(($entry+1))/$ecnt]" - - bk_parse_baka_entry "${bk_all_entries[iter-1]}" - bk_get_entry_data "${bk_all_entries[iter-1]}" 'files' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'exclude' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'command' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'mysql' - bk_get_entry_data "${bk_all_entries[iter-1]}" 'postgres' - [ "$verbose_test" ] && echo "Parse entry: OK" - - # Files checking. - for path in "${bk_files[@]}" - do - if [ -f "$path" ] || [ -d "$path" ]; then - [ "$verbose_test" ] && echo "Files: OK" - else - bk_err "$path: path does not exist" - fi - done - - # MySQL checking. - [ "$db_port" ] || db_port=3306 # Set default MySQL port. - for reqs in "${bk_mysql[@]}" - do - bk_parse_db_reqs "$reqs" - - [ "$verbose_test" ] && \ - echo -n "Checking database $db_name@$db_host (MySQL): " - - echo 'show variables like "%version%";' \ - | mysql \ - --host="$db_host" \ - --port="$db_port" \ - "$db_name" \ - --user="$db_user" \ - --password="$db_pass" > /dev/null - - [ "$verbose_test" ] && echo "OK" - done - - # PostgreSQL checking. - for reqs in "${bk_postgres[@]}" - do - bk_parse_db_reqs "$reqs" - - [ "$verbose_test" ] && \ - echo -n "Checking database: $db_name@$db_host (PostgreSQL): " - - [ "$db_port" ] || db_port=5432 # Set default PostgreSQL port. - export PGPASSWORD="$db_pass" - - echo 'select version();' \ - |psql \ - --host="$db_host" \ - --port="$db_port" \ - --dbname="$db_name" \ - --username="$db_user" \ - --no-password > /dev/null - - [ "$verbose_test" ] && echo "OK" - unset PGPASSWORD - done - - let iter++ # Increase iterator for entry counter. - echo # Just print new line. - done - - if [ "$remote" == 'none' ]; then - : - else - echo "-> Remote server testing ..." - echo 'This is baka test file' > ./testfile - [ "$verbose_test" ] && echo "Test file uploading ..." - case "$remote" in - rsync) - bk_upload_via_rsync ./testfile - [ "$verbose_test" ] && \ - echo "Test file uploaded successfully." - [ "$verbose_test" ] && \ - echo "Removing test file ..." - ssh -p "$ssh_port" "${ssh_uri%%:*}" "rm ${ssh_uri##*:}/testfile" - ;; - s3) - bk_upload_via_s3cmd ./testfile - [ "$verbose_test" ] && \ - echo "Test file uploaded successfully." - [ "$verbose_test" ] && \ - echo "Removing test file ..." - s3cmd -pq --no-progress del "$s3_uri"/testfile - ;; - esac - rm ./testfile - echo - fi - echo "Test is successful!" -} - - -# BAKA SHOW -############# - -bk_edit_help() { -# TODO -cat << EOF -Show configuration. - -Usage: baka show [--help | help] [] - -Options: - --help, help print this message and exit. - -Configuration file prints without comments. To view $main_conf just run -'baka show'. Also you can view a specific entry passed as argument. -EOF -exit 0 - -} -bk_show_config() { - bk_read_config "$main_conf" - exit 0 -} - -# BAKA EDIT -############# - -bk_edit_help() { -# TODO -cat << EOF -Edit configuration in default editor. - -Usage: baka edit [--help | help] [] - -Options: - --help, help print this message and exit. - -For edit $main_conf just run 'baka edit'. Also you can edit -a specific entry passed as argument. -EOF -exit 0 -} - -bk_edit_config() { - - get_selected_editor() { - source $HOME/.selected_editor - echo $SELECTED_EDITOR - } - - # Detect default editor. - if [ "$EDITOR" ]; then - local e=$EDITOR - elif [ -f $HOME/.selected_editor ]; then - local e="$(get_selected_editor)" - elif [ -f /usr/bin/select-editor ]; then - select-editor - local e="$(get_selected_editor)" - else - local e=/usr/bin/vi - fi - - # Open file in editor. - echo "Edit $main_conf" - "$e" "$main_conf" -} - -# BAKA REMOVE -# Remove old backups. -####################### - - -bk_remove_backups_help() { -cat << EOF -Remove old local backups (older than $livetime days). -Backup livetime is set in $main_conf - -Usage: baka remove [--help | help] [-f|--force] - -Options: - -f, --force force remove. - --help, help show this message and exit. -EOF -exit 0 -} - -bk_delete_files() { - # DELETE FILES. - # - # $1 is directory for search. - bk_log -n "Removing old backups (older than $livetime days) ..." - bk_log -n "Removing files from directory: $1" - find "$1" -regextype posix-extended \ - -regex "(.+)\.(tar.gz|sql.gz|psql.gz)" \ - -type f -mtime +$livetime -delete -print | bk_log - echo '(empty output -- no files deleted)' -} - -bk_get_dest_dirs() { - # Return destination directories list for remove_backups() - # function. - bk_find_entries # Get entries list. - - for entry in "${bk_all_entries[@]}" - do - bk_parse_baka_entry "$entry" - dest_dirs+=("$dest_dir") - done - - # Remove doubles. - local temp="$(\ - tr ' ' '\n' <<< "${dest_dirs[@]}" | sort -u | tr '\n' ' ' \ - )" - dest_dirs=() - for item in $temp - do - dest_dirs+=("$item") - done -} - -bk_remove_backups() { - - while (( "$#" )) - do - case "$1" in - -f|--force) force_remove=1; shift;; - --help|help) - bk_remove_backups_help;; - *) bk_err -s "bad option: $1";; - esac - done - - bk_get_dest_dirs # Get $dest_dirs - - if [ "$force_remove" ]; then - echo "Removing forced!" - for dir in "${dest_dirs[@]}" - do - bk_delete_files "$dir" - done - exit 0 - fi - - # Show prompt. - echo \ - "Files older than $livetime days will be removed. This action cannot be undone." - echo "Use '--force' option to remove files without prompt." - echo - if bk_yn_dialog "Continue?" - then - for dir in "${dest_dirs[@]}" - do - bk_delete_files "$dir" - done - else - echo 'Abort.'; exit 1 - fi -} - -# ARG PARSER -########################################################################## - -bk_parse_main_conf # Read configuration before anything. - -# Parse arguments. -[[ "$@" ]] || bk_print_help - -while (( "$#" )) -do - case "$1" in - backup) - shift; bk_do_backup "$@"; shift "$#";; - list) - shift; bk_show_list "$@"; shift "$#";; - test) - shift; bk_do_test "$@"; shift "$#";; - show) - bk_show_config; shift;; - edit) - bk_edit_config; shift;; - remove) - shift; bk_remove_backups "$@"; shift "$#";; - --version) - bk_print_version;; - --help|help) - bk_print_help;; - *) - bk_err -s "Unknown command: $1\nSee 'baka help' or 'man baka' for info." - esac -done diff --git a/etc/baka/baka.conf b/etc/baka/baka.conf new file mode 100644 index 0000000..48a551c --- /dev/null +++ b/etc/baka/baka.conf @@ -0,0 +1,80 @@ +# * baka 0.2.0 +# This is baka.conf example. See baka(1) for more info. + +# Path to lookup Entries. +entries = /etc/baka/entries + +# Log file. Default: /var/log/baka +log = /var/log/baka + +# Log format. +# +# Log format actually is `date` format string. See date(1) or +# 'date --help' for info. +# +# Special syntax (palceholdes): +# {log} will be replaced with actual log string. +# +# Result for this example: +# [21 Jul 2021 22:53:56 +0300] Backup [Started] +log_format = [%d %b %Y %T %z] {log} + +# Filename format. +# +# Special syntax (placeholders): +# {name} replaces with file, directory or database name. +# {prefix} replaces with prefix specified in Entry file +# or by autoprefix (based on Entry file name). +# +# Examples: +# myprefix_myfiles_20210722-2110.tar.gz +# myprefix_mydb_20210722-2110.sql.gz +filename_format = {prefix}{name}_%Y%m%d-%H%M + +# Automatic prefix for backups. +# +# Prefix is based on Entry file name. +# You can override autoprefix into Entry variable 'prefix'. +autoprefix = true + +# Allow Bash command executing. +# +# Default: false +# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +# @@ IT IS POTTENTIALLY DANGEROUS! @@ +# @@ DO NOT USE UNTRUSTED COMMANDS IN ENTRY! @@ +# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +allow_commands = false + +# Local storage. +# +# This global 'local' value can be overrided in Entry file. +# Backups will be saved into 'local' directory. +#local = /backups + +# Create directory per date. +# +# For example: backup 2022-01-01 will be saved in path +# /backups/example.com/2022-01-01/example.com_mybackup_20220101-0000.sql.gz +# instead of /backups/example.com/example.com_mybackup_20220101-0000.sql.gz +# +# `dir_per_date` value is interpretated as path. Use '/' to create nested +# hierarсрy. '%Y/%m/%d' will expand to: +# /backups/example.com/2022/01/01/example.com_mybackup_20220101-0000.sql.gz +dir_per_date = %Y-%m-%d + +# Remote storage. +# +# Uses URI format: [scheme]://[user[:password]]@[host[:port]][/path] +# +# Schemas can be: +# none no remote storage. +# rsync copy files to remote storage via rsync over SSH. +# s3 copy files to Amazon S3 or S3 compatible storage via s3cmd. +remote = none + +# Local backups live time (in days). +# +# Used by 'baka remove' command. Files older than 'livetime' days will be +# removed. +livetime = 30 diff --git a/etc/baka/entries/entry.example b/etc/baka/entries/entry.example index 01275c1..42ee606 100644 --- a/etc/baka/entries/entry.example +++ b/etc/baka/entries/entry.example @@ -1,79 +1,51 @@ -# This is an entry. This is part of the configuration of the "baka" -# backup software. Here you can set the parameters for backuping the -# requred data set. See examples below. -# -# See `man baka` for more info or visit http://nixhacks.net/baka +# * baka 0.2.0 +# This is Entry file example. See baka(1) for more info. -# Destination dir. +# Local storage. # -# This is this required parameter. It will be used if the `local` -# parameter is not specified in main.conf. You can specify it only -# once in an entry. -# -# This is the only required parameter. And the only one that can be -# specified only once. In addition to the destination folder, at -# least one variable with data must be specified. See below. -dest = /home/user/backups +# Specify path to store local backups. +local = /srv/example.org/backups -# Files or directories to backup. +# Remote storage. # -# Syntax: -# files = /path -# -# Also you can do this: -# files = /path/1 path/2 -# -# You can add the `files` variable as many times as you like. -files = /var/www/html +# You can override global remote storage configuration there. +# See baka(1) or baka.conf example /etc/baka/baka.conf +#remote = rsync://backups@123.45.67.89:2022/home/backups -# Exclusions. +# Filename prefix. # -# This parameter is in addition to the `files` parameter. With its -# help you can specify which files or folders should be excluded -# from the backup. -# -# Syntax: -# List files and folders separated by commas without spaces on -# one line: -# exclude = env,logs,__pycache__ -# -# Or split them into multiple lines: -# exclude = env -# exclude = logs -# exclude = __pycache__ -exclude = env,logs,__pycache__ +# If you want override autoprefix set prefix there. +prefix = example.org_ -# Databases. +# Archive files adn directories. # -# baka can dump MariaDB/MySQL (shortcut 'mysql') and PostgreSQL -# (shortcut 'postgres') databases. +# Files be archived with tar into .tar.gz format. +#archive = /srv/http/example.org/public + +# Exclude. # -# Syntax: -# [dmbs] = host:port:database:user:password +# You can exclude some files and directories from archive. +# Items can be set in one line comma separated. +exclude = backups,logs,env,__pycache__,node_modules + +# Copy file or directory. # -# For example: -# postgres = localhost:5432:mydb:user:password +# Simple copy file of directory via 'cp' without compression. +# NOTE: 'exclude' varioable not affects on 'copy'. +#copy = /srv/http/example.org/data.zip + +# Database. # -# You can use shortened syntax too (use standart host and port): -# [dmbs] = host:database:user:password -# [dmbs] = database:user:password -# -# NOTE: that due to the nature of the parser, colons and hash -# characters cannot be used in the database password. -#mysql = localhost:port:database:user:password -#postgres = localhost:port:database:user:password +# Dump MySQL/MariaDB or PostgreSQL database. Requisites must be passed as +# DB URI. For example: +# [schema]://[user[:password]]@[host[:port]]/[database] +# Available schemas: +# For MySQL/MariaDB: mysql, mariadb +# For PostgreSQL: postgresql, postgres, psql +database = postgres://user:password@localhost:5432/database # Commands. # -# In addition to files and databases, baka can execute an -# arbitrary Bash command for you. -# -# For example: -# command = echo "Hello, World!" -# -# This feature is disabled by default and must be added in -# the main config. -# DON'T USE UNTRUSTED COMMANDS IN ENTRY! -# This feature is pottentially dangerous. Make sure that access -# to the entries is restricted. -#command = echo "Are you sure you want to enable it?" +# You can execute any Bash command from Entry. This feature is disabled +# by default. +#command = /srv/http/example.org/dump.py --save /srv/http/example.org/data.zip diff --git a/etc/baka/main.conf b/etc/baka/main.conf deleted file mode 100644 index fb476f0..0000000 --- a/etc/baka/main.conf +++ /dev/null @@ -1,69 +0,0 @@ -# Log file. Default: /var/log/baka -log = /var/log/baka - -# Datetime format in log file. See 'man date'. -log_df = %d %b %Y %T %z - -# Log format. -# -# Syntax: -# %time -# will be replaced with 'log_df' formatted datetime. -# %log -# will be replaced with log string. -# -# Result for this example: -# [21 Jul 2021 22:53:56 +0300] Backup [Started] -log_format = [%time] %log - -# Datetime format. See 'man date'. -# This datetime is used for filenames. Example: -# backup_myfiles_20210722-2110.tar.gz -df = %Y%m%d-%H%M - -# Archive name format. -# -# Syntax: -# %type -# 'backup' for files or 'dump' for databases. -# %name -# directory or database name. -# %time -# datetime formatted by 'df' option. -# -# Examples: -# backup_myfiles_20210722-2110.tar.gz -# dump_mydb_20210722-2110.sql.gz -nf = %type_%name_%time - -# Allow comand executing. -# -# Default: false -# -# IT IS POTTENTIALLY DANGEROUS! -allow_commands = false - -# Remote storage configuration. -# -# Remote tranport. Can be 'rsync', 's3' or 'none'. -remote = none - -# URI for connection via rsync. -#ssh_uri = user@server:/path - -# SSH port on remote server. Default: 22 -#ssh_port = 22 - -# URI for S3 bucket. See 'man s3cmd' for more info. -#s3_uri = s3://mybucket/backups - -# Local storage configuration. -# -# If 'local' is set, the 'dest' options in entries will -# be ignored. All backups will be saved into 'local' folder. -#local = /path/to/backups_dir - -# Backup live time. -# -# Used by 'baka remove' command. -livetime = 30 diff --git a/make_dist b/make_dist deleted file mode 100755 index 716ceb9..0000000 --- a/make_dist +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bash -# Make DEB package! - -[ -d $PWD/dist/ ] && rm -rf dist/ -[ -f baka_*.deb ] && rm baka_*.deb - -# DIRS - -prefix=dist -mkdir -p $prefix/DEBIAN -mkdir -p $prefix/usr/bin -mkdir -p $prefix/usr/share/bash-completion/completions -mkdir -p $prefix/usr/share/man/man1 -cp -r etc/ dist/ - -# BINARIES - -cp baka dist/usr/bin/baka && chmod +x dist/usr/bin/baka -cp completion dist/usr/share/bash-completion/completions/baka - -# MANPAGES - -md2man ./manpage.md dist/usr/share/man/man1/baka.1 -sed -i 's%\.TH "" "" "" "" ""%\.TH BAKA 1 "31 July 2021" "baka 0.1.2"%' dist/usr/share/man/man1/baka.1 -gzip -9 dist/usr/share/man/man1/baka.1 - -# DEBIAN/* - -ver=0.1.2 - -cat > dist/DEBIAN/control << EOF -Package: baka -Version: $ver -Section: admin -Priority: optional -Maintainer: gd -Homepage: http://nixhacks.net/baka -Architecture: all -Depends: rsync, s3cmd -Description: Simple backuping tool. - Backup files and MySQL/MariaDB and PostgreSQL databases. -EOF - -cat > dist/DEBIAN/changelog << EOF -baka (0.1.2) testing; urgency=medium - - * Initial release. - - -- gd Sat, 31 Jul 2021 15:22:17 +0300 -EOF - -cat > dist/DEBIAN/copyright << EOF -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: baka -Upstream-Contact: http://nixhacks.net/baka -Source: http://nixhacks.net/baka - -Files: * -Copyright: 2021 gd -License: GPL-3.0+ - -Files: debian/* -Copyright: 2021 gd -License: GPL-3.0+ - -License: GPL-3.0+ - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - . - This package 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, see . - . - On Debian systems, the complete text of the GNU General - Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". - -# Please also look if there are files or directories which have a -# different copyright/license attached and list them here. -# Please avoid picking licenses with terms that are more restrictive than the -# packaged work, as it may make Debian's contributions unacceptable upstream. -# -# If you need, there are some extra license texts available in two places: -# /usr/share/debhelper/dh_make/licenses/ -# /usr/share/common-licenses/ -EOF - -dpkg-deb --build ./dist baka_$ver.deb diff --git a/manpage.md b/manpage.md deleted file mode 100644 index facd69e..0000000 --- a/manpage.md +++ /dev/null @@ -1,210 +0,0 @@ -## NAME -baka \- do files and databases backup. - -## SYNOPSIS -baka \[\-\-version | -V\] \[\-\-help | help\] \ \[\...\] - -## DESCRIPTION -baka allows you to backup MariaDB/MySQL and PostgreSQL directories and databases. - -baka is based on handling entries. Processing can be started at once for everyone entries, either only for the selected ones, or for all but the excluded ones. - -baka implements only the mechanism for creating archives with files and database dumps and transferring these files to remote storage using third-party tools (such as rsync, s3cmd). To perform scheduled backups, the command baka it is recommended to install it in the operating system task scheduler. For example cron. Examples of usage are in the section EXAMPLES. - -From the above points and the possibility of customizing a separate entry, it follows that with using baka, you can configure the backup system quite flexibly. - -**ATTENTION**: Despite the laudatory description above, baka should not be regarded as professional backup solutions. Please don't use baka in production-like environments. - -### Basic concepts: - -**Entry** - -Entity describing the elements to be acted upon. There can be any number of these entities. For a more detailed description, see section CONFIGURATION FILES. - -**Remote transport** - -A method for delivering files to a remote storage. In version 0.1.0 it could be sync using rsync, upload files to S3 compatible storage with using the s3cmd utility or 'none', which means refusing to upload files to remote storage. - -## COMMANDS - -**backup** - -Do backup right now. Available options: - - -i, --ignore= run backup for all entries, except ignored. - -e, --entry= run backup for selected entry. - --local force local backup. - --no-verify don't check archives integrity. - --remove remove old backups (forced). - --help, help print help message and exit. - -**list** - -List backup entries. Available options: - - -v, --verbose print entries list verbosely (default). - -s, --short short format (names). - -S short format (pathes). - --help, help print help message and exit. - -**test** - -Test configuration incude all entries. Available options: - - -v, --verbose print output verbosely. - --help, help print help message and exit. - -**show** - -Print main configuration. - -**edit** - -Edit main configuration in default text editor. - -**remove** - -Remove old local backups (older than $livetime days). Backup livetime is set in main.conf. - - -f, --force force remove. - --help, help show this message and exit. - -## CONFIGURATION FILES -### MAIN.CONF - -Default path: /etc/baka/main.conf. This contains the values for the global baka settings. - -Required parameters: - - log log file (/var/log/baka). - log_df time format in the log. Example: %d %b %Y %T %z - More details in 'man date'. - log_format format of the log line. Has special syntax. - Example: - [%time] %log - Here: - %time time formatted by 'log_df'. - %log data string - df time format in file names. See 'man date' - nf file name format. Has special syntax. - Example: - %type_%name_%time - Here: - %type content type. The archive name will be - added word 'dump' for database - or 'backup' for files. - %name is the name of a directory or database. - %time time formatted with 'df'. - remote remote transport. - livetime the lifetime of old backups (number of days). - -Optional parameters: - - allow_commands flag for executing commands from entries. - Default: Disabled - ssh_uri URI for SSH connection for rsync. - ssh_port specify port if different from standard 22. - s3_uri URI to connect to the S3 repository. - local local folder to save all backups. Everything - 'dest' in entries will be ignored. - -### ENTRIES - -By default entries is stored into /etc/baka/entries/ directory in separated files. File name does not matter. - -Here you can set the parameters for backuping the requred data set. See examples below. - -**DESTINATION DIR** - -This is this required parameter. It will be used if the `local` parameter is not specified in main.conf. You can specify it only once in an entry. - -This is the only required parameter. And the only one that can be specified only once. In addition to the destination folder, at least one variable with data must be specified. - -Example: - - dest = /home/user/backups - -**FILES** - -Files or directories to backup. Syntax: - - files = /path - -Also you can do this: - - files = /path/1 path/2 - -You can add the `files` variable as many times as you like. - -**EXCLUSIONS** - -This parameter is in addition to the `files` parameter. With its help you can specify which files or folders should be excluded from the backup. - -List files and folders separated by commas without spaces on one line: - - exclude = env,logs,__pycache__ - -Or split them into multiple lines: - - exclude = env - exclude = logs - exclude = __pycache__ - -**DATABASES** - -baka can dump MariaDB/MySQL (shortcut 'mysql') and PostgreSQL (shortcut 'postgres') databases. - -Syntax: - - [dmbs] = host:port:database:user:password - -For example: - - postgres = localhost:5432:mydb:user:password - -You can use shortened syntax too (use standart host and port): - - [dmbs] = host:database:user:password - [dmbs] = database:user:password - -**NOTE:** That due to the nature of the parser, colons and hash characters cannot be used in the database password. - -**COMMANDS** - -In addition to files and databases, baka can execute an arbitrary Bash command for you. For example: - - command = echo "Hello, World!" - -This feature is disabled by default and must be added in the main config. DON'T USE UNTRUSTED COMMANDS IN ENTRY! This feature is pottentially dangerous. Make sure that access to the entries is restricted. - -## EXAMPLES - -An example of using baka. For example, there are two files in /etc/baka/entries/: example.org and example.com. Files content: - -example.org: - - dest = /home/user/backups/example.org - files = /srv/example.org/public - exclude = logs, cache - -example.com: - - dest = /home/user/backups/example.com - files = /srv/example.org/public - files = /srv/example.org/storage - mysql = mydb: dbuser: password - -In order to start backing up these sites at different times, let's add two tasks to crontab: - - 00 00 * * * /usr/bin/baka backup --entry=example.com - 00 00 */2 * * /usr/bin/baka backup --entry=example.org - -Likewise, you can start backing up more entries by adding exceptions or specifying specific entries. For example like this: - - baka backup --ignore=example.org --ignore=example.com - -## BUGS -Report bugs to or - -## AUTHOR -gd (gechandev@gmail.com) diff --git a/manpage.ru.md b/manpage.ru.md deleted file mode 100644 index 54f805d..0000000 --- a/manpage.ru.md +++ /dev/null @@ -1,211 +0,0 @@ -## НАЗВАНИЕ -baka \- создание резервных копий файлов и баз данных. - -## ОБЗОР -baka \[\-\-version | -V\] \[\-\-help | help\] \ \[\...\] - -## ОПИСАНИЕ -baka позволяет создавать резервные копии каталогов и баз данных MariaDB/MySQL и PostgreSQL. - -Работа baka базируется на обработке записей (entries). Обработка может быть запущена сразу для всех записей, либо только для выбранных, либо для всех, кроме исключенных. - -baka реализует только механизм создания архивов с файлами и дампами баз данных и передачи этих файлов в удаленное хранилище с помощью сторонних инструментов (таких как rsync, s3cmd). Для выполнения резервного копирования по расписанию, команду baka рекомендуется установить в планировщике задач операционной системы. Например cron. Примеры использования находятся в разделе ПРИМЕРЫ. - -Из вышеперечисленных пунктов и возможности настройки отдельной записи следует, что с помощью baka можно довольно гибко настроить систему резервного копирования. - -**ВНИМАНИЕ**: Несмотря на красивое описание выше, baka не следует рассматривать как профессиональные решения для резервного копирования. Пожалуйста, не используйте baka в продакшн среде. - -### Основные понятия: - -**Entry** - -Сущность, описывающая элементы, с которыми нужно выполнять действия. Таких сущностей может быть любое количество. Более подробное описание см. В разделе КОНФИГУРАЦИОННЫЕ ФАЙЛЫ. - -**Remote transport** - -Способ доставки файлов в удаленное хранилище. В версии 0.1 это может быть синхронизация с помощью rsync, загрузка файлов в S3-совместимое хранилище с помощью утилиты s3cmd или «none», что означает отказ от загрузки файлов в удаленное хранилище. - -## КОМАНДЫ - -**backup** - -Сделать резервную копию прямо сейчас. Доступные опции: - - -i, --ignore= запустить резервное копирование для всех записей, - кроме игнорируемых. - -e, --entry= запустить резервное копирование выбранной записи. - --local принудительное локальное резервное копирование. - --no-verify не проверять целостность архивов. - --remove удалить старые резервные копии (принудительно). - --help, help напечатать справочное сообщение и выйти. - -**list** - -Список доступных entries. Доступные опции: - - -v, --verbose выводить список записей подробно (по умолчанию). - -s, --short короткий формат (имена). - -S короткий формат (пути). - --help, help напечатать справочное сообщение и выйти. - -**test** - -Проверка конфигурации main.conf и всех entries. Доступные опции: - - -v, --verbose выводить на печать подробный вывод. - --help, help напечатать справочное сообщение и выйти. - -**show** - -Распечатать основную конфигурацию (main.conf). - -**edit** - -Открыть main.conf в текстовом редакторе по умолчанию. - -**remove** - -Удалить старые локальные резервные копии (старше чем $livetime дней). Время жизни резервной копии устанавливается в main.conf. - - -f, --force удалить без подтверждения. - --help, help напечатать справочное сообщение и выйти. - -## КОНФИГУРАЦИОННЫЕ ФАЙЛЫ -### MAIN.CONF - -Путь к файлу: /etc/baka/main.conf. Он содержит значения для глобальных настроек baka. - -Обязательные параметры: - - log файл лога (/var/log/baka). - log_df формат времени в логе. Пример: %d %b %Y %T %z - Подробнее в 'man date'. - log_format формат строки лога. Имеет специальный синтаксис. - Пример: - [%time] %log - Здесь: - %time время, отформатирвованное по 'log_df'. - %log строка с данными - df формат времени в именах файлов. См. 'man date' - nf формат имён файлов. Имеет специальный синтаксис. - Пример: - %type_%name_%time - Здесь: - %type тип содержимого. К имени архива будет - добавлено слово 'dump' для базы данных - или 'backup' для файлов. - %name имя директории или базы данных. - %time время, отформатированное по 'df'. - remote remote transport. - livetime время жизни старых бэкапов (количество дней). - -Опциональные параметры: - - allow_commands флаг для выполнения команд из entries. - По-умолчанию: отключено - ssh_uri URI для подключения по SSH для rsync. - ssh_port указать порт, если отличается от стандартного 22. - s3_uri URI для подключения к хранилищу S3. - local локальная папка для сохранения всех бэкапов. Все - 'dest' в entries будут проигнорированы. - -### ЗАПИСИ (ENTRIES) - -Записи хранятся в каталоге /etc/baka/entries/ в отдельных файлах. Имя файла значения не имеет. - -В них вы можете установить параметры резервного копирования нужного набора данных. См. примеры ниже. - -**DESTINATION DIR** - -Локальная целевая папка. В неё будут сохранены данные, которые описаны в записи. Это обязательный параметр. Он будет использоваться, если параметр `local` не указан в main.conf. Вы можете указать его только один раз в записи. - -Это единственный обязательный параметр. И единственный, который можно указать только один раз. В дополнение к целевой папке должна быть указана хотя бы одна переменная с данными. - -Пример: - - dest = /home/user/backups - -**FILES** - -Файлы или каталоги для резервного копирования. Синтаксис: - - files = /path - -Также вы можете сделать так: - - files = /path/1 /path/2 - -Вы можете добавлять переменную `files` сколько угодно раз. - -**EXCLUSIONS** - -Этот параметр является дополнением к параметру files. С его помощью вы можете указать, какие файлы или папки следует исключить из резервной копии. - -Перечислить файлы и папки через запятую без пробелов в одной строке: - - exclude = env,logs,__ pycache__ - -Или разделите их на несколько строк: - - exclude = env - exclude = logs - exclude = __pycache__ - -**DATABASES** - -baka может сделать базы данных MariaDB/MySQL (ярлык mysql) и PostgreSQL (ярлык postgres). - -Синтаксис: - - [dmbs] = host:port:database:user:password - -Например: - - postgres = localhost:5432:mydb:user:password - -Вы также можете использовать сокращенный синтаксис (используйте стандартный хост и порт): - - [dmbs] = host:database:user:password - [dmbs] = database:user:password - -**ПРИМЕЧАНИЕ:** Из-за особенностей парсера двоеточия и символы решетки не могут использоваться в пароле базы данных. - -**COMMANDS** - -Помимо файлов и баз данных, baka может выполнять для вас произвольную команду Bash. Например: - - command = echo "Привет, мир!" - -Эта функция отключена по умолчанию и может быть включена в main.conf. НЕ ИСПОЛЬЗУЙТЕ НЕДОВЕРЕННЫЕ КОМАНДЫ В ENTRY! Эта функция потенциально опасна. Убедитесь, что доступ к записям ограничен. - -## ПРИМЕРЫ - -Пример использования baka. Например, есть два файла в /etc/baka/entries: example.org и example.com. Содержимое файлов: - -example.org: - - dest = /home/user/backups/example.org - files = /srv/example.org/public - exclude = logs,cache - -example.com: - - dest = /home/user/backups/example.com - files = /srv/example.org/public - files = /srv/example.org/storage - mysql = mydb:dbuser:password - -Для того, чтобы заапускать резервное копирование этих сайтов в разное время, добавим в crontab две задачи: - - 00 00 * * * /usr/bin/baka backup --entry=example.com - 00 00 */2 * * /usr/bin/baka backup --entry=example.org - -Точно таким же образом можно запускать резервное копирование большего числа entries, добавляя исключения или указывая конкретные entries. Например так: - - baka backup --ignore=example.org --ignore=example.com - -## ОШИБКИ -Сообщайте об ошибках на или -ё -## АВТОР -gd (gechandev@gmail.com) diff --git a/mkdist b/mkdist new file mode 100755 index 0000000..0d90586 --- /dev/null +++ b/mkdist @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Make DEB package! + +ver=0.2.0 +credit='ge ' + +[ -d $PWD/dist/ ] && rm -rf dist/ +[ -f baka_*.deb ] && rm baka_*.deb + +# DIRS + +prefix=dist +mkdir -p $prefix/DEBIAN +mkdir -p $prefix/usr/bin +mkdir -p $prefix/usr/share/bash-completion/completions +mkdir -p $prefix/usr/share/man/man1 +cp -r etc/ $prefix + +# BINARIES + +cp src/baka $prefix/usr/bin/baka && chmod +x $prefix/usr/bin/baka +cp src/completion $prefix/usr/share/bash-completion/completions/baka + +# MANPAGES + +pandoc --from=markdown --to=man --standalone \ + docs/baka.1.md --output=$prefix/usr/share/man/man1/baka.1 + +c=`date +'%d %b %Y'` +sed -i "s%\.TH \"\" \"\" \"\" \"\" \"\"%\.TH BAKA 1 \"$c\" \"baka $ver\"%" $prefix/usr/share/man/man1/baka.1 +gzip -9 $prefix/usr/share/man/man1/baka.1 + +# DEBIAN/* + +cat > $prefix/DEBIAN/control << EOF +Package: baka +Version: $ver +Section: admin +Priority: optional +Maintainer: $credit +Homepage: http://nixhacks.net/baka +Architecture: all +Depends: rsync, s3cmd +Description: Simple backuping tool. + Backup files and MySQL/MariaDB and PostgreSQL databases. +EOF + +cat > $prefix/DEBIAN/changelog << EOF +baka (0.2.0) testing; urgency=medium + + * Code refactoring. + + WARNING! All changes in this version breaks backward capatibility! + + # CLI + + New options: + + - '--config'. Specify your baka.conf file. + - 'backup --dry-run'. Test configuration without backup. + - 'remove --list'. Print files to delete. + + Changed: + + - 'list' command now has two options '--verbose' and '--help'. Verbose output show Entry name, path and local and remote storages in table view. + + Removed: + + - 'show' command. View configuration file directly instead. + - 'edit' command. Edit configuration file directly instead. + - 'test' command. Use 'backup --dry-run' instead. + + # baka.conf (former name: main.conf) + + Added: + + - Default parameter values now is built in baka. Override it in baka.conf + - 'entries' variable. You can set entries directory in baka.conf + - 'filename_format'. Special formatting for filenames. + - 'autoprefix'. Add automatic prefix to filenames (based on entry file name). + - 'dir_per_date'. Backups path format. See baka(1). + + Changed: + + - 'dest' now is 'local' and can be used in baka.conf and entries. + - 'remote' now is URI (like '[scheme]://[user[:password]]@[host[:port]][/path]') and can be used in baka.conf and entries. + + Removed: + + - 'nf', use 'filename_format' instead + - 'df', use 'filename_format' + - 'log_df', use 'log_format' istead + + # Entries + + Added: + + - 'local'. This is renamed 'dest'. + - 'remote'. Now you can specify remote storage per entry. + - 'prefix'. Override filename prefix. + - 'archive'. Renamed 'files'. Archive files into **.tar.gz**. + - 'copy'. Simple file copy without compression. + - 'database'. Replacement for 'mysql' and 'postrges' variables. Provide DB URI. + + Removed: + + - 'dest'. Use 'local' instead. + - 'mysql' and 'postgres' variables. Use 'database' instead. + - 'files' variable. Use 'archve' instead. + +-- $credit `date -R` + +baka (0.1.2) testing; urgency=medium + + * Initial release. + + -- $credit Sat, 31 Jul 2021 15:22:17 +0300 +EOF + +cat > $prefix/DEBIAN/copyright << EOF +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: baka +Upstream-Contact: http://nixhacks.net/baka +Source: http://nixhacks.net/baka + +Files: * +Copyright: `date +%Y` $credit +License: GPL-3.0+ + +Files: debian/* +Copyright: `date +%Y` $credit +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package 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, see . + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +# Please avoid picking licenses with terms that are more restrictive than the +# packaged work, as it may make Debian's contributions unacceptable upstream. +# +# If you need, there are some extra license texts available in two places: +# /usr/share/debhelper/dh_make/licenses/ +# /usr/share/common-licenses/ +EOF + +dpkg-deb --build $prefix baka_$ver.deb diff --git a/src/baka b/src/baka new file mode 100755 index 0000000..20ffaf4 --- /dev/null +++ b/src/baka @@ -0,0 +1,1072 @@ +#!/usr/bin/env bash + +########################################################################## +# +# baka +# +# Copyright (C) 2021 ge . +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see . +# +########################################################################## + +set -o errexit # Exit if error occurs +baka_version=0.2.0 + +# BAKA BASE CONFIGURATION +######################### + +# Configuration lookup pathes. +# baka_conf is system wide configuration +baka_conf=/etc/baka/baka.conf + +# User's local configuration overrides system wide configuration. +baka_local_conf=${HOME}/.config/baka/baka.conf + +# DEFAULTS +# See baka(1) +entries=/etc/baka/entries +log=/var/log/baka +log_format='[%d %b %Y %T %z] {log}' +filename_format='{prefix}{name}_%Y%m%d-%H%M' +autoprefix=true +allow_commands=false +backups_livetime=30 + +########################################################################## +# SERVICE FUNCTIONS +########################################################################## + +bk_help() { + case "$1" in + backup) bk_do_backup_help;; + list) bk_list_entries_help;; + remove) bk_remove_old_backups_help;; + *) : ;; + esac + cat <<- EOF + Usage: baka [--version] [--help | help] [-c | --config=] + [...] + + Commands and options: + backup do backup right now. + list list backup entries. + remove remove old backups. + -c, --config= source baka.conf file. + --version print version and exit. + --help, help print this help message and exit. + + See 'man baka' or 'baka help' for more info. + + , + (#`皿´) + + Senpai wants you to do backups! + EOF + exit 0 +} + +bk_do_backup_help() { + cat <<- EOF + Do backup. + + Usage: baka backup [--help | help] [-i | --ignore=] + [-e | --entry=] [--local] [--no-verify] + [--dry-run] [--remove] + Options: + -i, --ignore= run backup for all entries, except ignored. + -e, --entry= run backup for selected entry. + --local force local backup. + --no-verify don't check archives integrity. + --dry-run configuration test. + --remove remove old backups (forced). + --help, help print this message and exit. + EOF + exit 0 +} + +bk_list_entries_help() { + cat <<- EOF + List backup entries. + + Usage: baka list [--help | help] [-v | --verbose] + + Options: + -v, --verbose list entries verbosely (table format). + --help, help print this message and exit. + EOF + exit 0 +} + +bk_remove_old_backups_help() { + cat <<- EOF + Remove old local backups (older than $livetime days). + Backup livetime is set in $conf + + Usage: baka remove [--help | help] [-l | --list] [-f|--force] + + Options: + -l, --list print a list of files to be deleted. + -f, --force force remove. + --help, help print this message and exit. + EOF + exit 0 +} + +bk_version() { + cat <<- EOF + baka $baka_version + Copyright (C) 2021 ge . + License GPLv3+: GNU GPL version 3 or later . + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + EOF + exit 0 +} + +########################################################################## +# LOGGING AND ERROR MESSAGING +########################################################################## + +bk_err() { + # Show error message and exit from script. + # Arguments: + # $@ - error message (can be partial). + echo -e "${0##*/}: $@" | sed 's/^ *//g' >&2 + exit 1 +} + +bk_cli_err() { + # Bad command/option error message. + # Arguments: + # $1 - option name. + echo -e "${0##*/}: $1: unknown option\n" \ + "\bSee 'baka help' or 'man baka' for info." >&2 +} + +bk_log() { + # Logger. Write log string into `log` file. + # Log is formatted by `log_format` value. + # Arguments: + # -p - print log message to STDOUT too. + # $@ - log message + + while (( "$#" )); do + case "$1" in + -p) local print=1; shift;; + *) local log_message="$@"; shift "$#";; + esac + done + + # Read STDIN if log message not sent as argument. + [[ "$log_message" ]] || local log_message="$(cat <&0)" + + # Don't write log if `log_message` is empty. + [[ "$log_message" == '' ]] && return 0 + + # Escape ine and remove ANSI characters + local escaped_log_message="$( \ + echo -e "$log_message" | + sed -r 's/\x1B\[(([0-9]+)(;[0-9]+)*)?[m,K,H,f,J]//g' | + sed 's/[\/&]/\\&/g')" + + while IFS= read -r log_line; do + [ "$print" ] && echo -e "$log_message" + date +"$log_format" | sed "s/{log}/$log_line/" >> "$log" + done <<< "$escaped_log_message" +} + +########################################################################## +# LOAD CONFIGURATION +########################################################################## + +bk_load_conf() { + # Load configuration file. Return clean config. + # Arguments: + # $1 - path to configuration file. + # Here is: + # - Remove spaces and tabs across '=' + # - Remove comments ('#') + # - Remove blank lines + # - Escape whitespaces and special characters (via printf) + local bk_config="$(sed \ + 's/[[:space:]]\+=[[:space:]]\+/=/g;/^#/d;/^$/d;s/#.*//g' <(\ + cat "$1" | grep = \ + ) | while read -r LN; do echo $(printf '%q' "$LN"); done)" + echo "$bk_config" +} + +bk_load_baka_conf() { + # Load, test and source baka.conf. + # + # Use `conf` value if it is set as CLI argument (`--config` option) + # or use local or global config file. See baka(1) DEFAULTS. + + # Lookup confiuration + if [ "$conf" ]; then + : # do nothing if configuration file is passed as CLI argument. + elif [ -f "$baka_local_conf" ]; then + conf="$baka_local_conf" # use local configuration if exists. + elif [ -f "$baka_conf" ]; then + conf="$baka_conf" # use global configuration if exists. + else + echo "${0##*/}: WARNING: no configuration files found." \ + "Default values will be used." >&2 + bk_log 'baka: WARNING: no configuration files found.' \ + 'Default values will be used.' + return 0 # Exit from function + fi + + local illegal="$( cat "$conf" | + egrep "^(prefix|archive|copy|exclude|database|command)=")" + if [[ "$illegal" != '' ]]; then + bk_err "$conf: configuration error: illegal variable $illegal" + fi + + # Load and source (via eval) configuration! + eval "$(bk_load_conf "$conf")" +} + +bk_load_entry() { + # Verify entry file. Return `loaded_entry` + # Arguments: + # $1 - entry filename. + + # Load entry content. + loaded_entry="$(bk_load_conf "$1")" + + # Check required parameters. + if ! egrep "archive|copy|database|command" <<< "$loaded_entry" > /dev/null; then + bk_err "$1: configuration error: no data to backup." + fi + + # Don't allow overwrite baka.conf variables + local illegal="$( \ + egrep "^(entries|log|log_format|filename_format|autoprefix|allow_commands|livetime|dir_per_date)=" \ + <<< "$loaded_entry")" + if [[ "$illegal" != '' ]]; then + bk_err "$1: configuration error: illegal variable $illegal" + fi +} + +########################################################################## +# PARSE ENTRIES +########################################################################## + +bk_parse_uri() { + # Universal URI parser. + + uri="$1" + schema="$(cut -d ':' -f 1 <<<"$uri")" + user="$(grep -Po '(?<=://)(.*)(?=@)' <<<"$uri" | cut -d ':' -f 1)" + password="$(passw=$(grep -Po '(?<=://)(.*)(?=@)' <<<"$uri"); \ + [[ $passw =~ .+:.+ ]] && echo ${passw##*:})" || true + host="$(grep -Po '(?<=@)(.*)' <<<"$uri" | + cut -d '/' -f 1 | cut -d ':' -f 1)" + port="$(prt=$(grep -Po '(?<=@)(.*)' <<<"$uri" | + cut -d '/' -f 1); [[ $prt =~ .+:[0-9]{1,} ]] && + echo ${prt##*:})" || true + path="$(pth=$(grep -Po '(?<=@)(.*)' <<<"$uri"); [[ $pth =~ :~ ]] && + cut -d ':' -f 2 <<<"$pth" || + grep -Po '(?<=(:[~/])|/)(.*)' <<<"$pth" | + xargs -I {} echo /{} | sed 's%//%/%g')" || true + + # Decode password + __urldecode() { + : "${*//+/ }"; echo -e "${_//%/\\x}"; + } + password="$(__urldecode "$password")" || true +} + +# Get parameters from entry. + +bk_get_entry_local() { + # Get 'local' from entry. + if [[ $(grep -c 'local=' <<< "$loaded_entry") == 0 ]]; then + bk_log -p "baka: WARNING: 'local' is not set. Use 'local' from $conf" + [ "$local" ] || { bk_err "No 'local' value in $conf"; } + entry_local="$local" # Apply global value. + return 0 + elif [[ $(grep -c 'local=' <<< "$loaded_entry") > 1 ]]; then + bk_err "Double 'local' parameter." + fi + # Get 'local' value. + entry_local="$(awk -F '=' '/^local=/ {print $2}' <<< "$loaded_entry" | + sed 's%\\%%g')" # seduce backslash for whitespases. + if [ ! -d "$entry_local" ]; then + bk_err "$1: $entry_local: destination dir does not exist." + fi +} + +bk_get_entry_remote() { + # Get 'remote' from entry. + if [[ "$(grep -c 'remote=' <<< "$loaded_entry")" == 0 ]]; then + bk_log "baka: WARNING: 'remote' is not set. Use 'remote' from $conf" + [ "$remote" ] || { bk_err "No 'remote' value in $conf"; } + entry_remote="$remote" # Apply global value. + return 0 + elif [[ "$(grep -c 'remote=' <<< "$loaded_entry")" > 1 ]]; then + bk_err "Double 'remote' parameter." + fi + entry_remote="$(awk -F '=' '/^remote=/ {print $2}' <<< "$loaded_entry")" + + local schema="$(cut -d ':' -f 1 <<<"$entry_remote")" + case "${schema,,}" in + rsync|s3|none) : ;; + *) br_err "Unsupported protocol $schema." + # TODO оставить тут контакт для feature request'ов + esac +} + +bk_get_entry_prefix() { + # Get 'prefix' from entry. + # Arguments: + # $1 -- entry file name. + if [[ "${autoprefix,,}" == true ]]; then + entry_prefix="${1##*/}_" + fi + + if grep 'prefix=' > /dev/null <<< "$loaded_entry"; then + entry_prefix="$(awk -F '=' '/^prefix=/ {print $2}' <<< "$loaded_entry")" + fi +} + +bk_get_entry_data() { + # This function gets to entry data and test it! + # + # Return array of valid entry parameter values. + # + # For example you wrote 3 different database requisites + # in /etc/baka/entries/example.conf: + # database = mysql://user1:pass1@host/database1 + # database = mysql://user2:pass2@host/database2 + # database = postgres://user1:pass1@host/database1 + # All requisites will be tested and you receive an array: + # bk_database = ( mysql://user1:pass1@host/database1 + # mysql://user2:pass2@host/database2 + # postgres://user1:pass1@host/database1 ) + # + # Parameters for this function: + # $1 - search query (e.g.: archive, database, copy, etc.). + + eval "entry_$1=()" # Set empty data to prevent dublicates. + + # Collect values. + local raw_values="$(grep "$1" <<< "$loaded_entry" | + awk -F '=' '{for(i=2;i<=NF;i++){printf "%s\n", $i}}')" + + while read -r value; do + # Check value + case "$1" in + archive|copy) [ "$value" ] && bk_test_file "$value";; + database) [ "$value" ] && bk_test_database "$value";; + *) : ;; + esac + + # Save values + eval "entry_$1+=("$value")" + done <<< "$raw_values" +} + +########################################################################## +# REMOTE +########################################################################## + +bk_check_rsync() { + if hash rsync 2>/dev/null; then + rsync_bin=/usr/bin/rsync + else + bk_err 'rsync executable not found.' + fi +} + +bk_check_s3cmd() { + if hash s3cmd 2>/dev/null; then + # If s3cmd is installed via OS package manager. + s3cmd_bin=/usr/bin/s3cmd + elif [ -f /usr/local/bin/s3cmd ]; then + # If s3cmd is installed globally via Python pip. + s3cmd_bin=/usr/local/bin/s3cmd + else + bk_err 's3cmd executable not found.' + fi +} + +bk_upload_via_rsync() { + # RSYNC + bk_check_rsync + local filename="$1" + [ "$port" ] || port=22 + uri="${user}@${host}:/${path}" + ssh -p "$port" "${user}@${host}" "mkdir -p ${path}/${dir_per_date}; exit" + rsync --contimeout=3 -a -e "ssh -p $port" "$filename" "${uri}/${dir_per_date}" | bk_log +} + +bk_upload_via_s3cmd() { + # S3 + bk_check_s3cmd + local filename="$1" + s3cmd -p --no-progress \ + put "$filename" "${entry_remote}/${dir_per_date}/" | bk_log +} + +bk_upload_file() { + # Select function to upload. + # + # $1 -- file passed to upload. + + [[ "${entry_remote,,}" == 'none' ]] && return 0 + + bk_parse_uri "$entry_remote" + + bk_log -p "Uploading ${1##*/} to remote server ..." + case "$schema" in + rsync) bk_upload_via_rsync "$1";; + s3) bk_upload_via_s3cmd "$1";; + esac + bk_log -p "Uploading ${1##*/} [Done]" +} + +########################################################################## +# DO BACKUP +########################################################################## + +bk_get_name() { + # Parse `filename_format` and return actiual filename. + local pref="$entry_prefix" + local name="${1##*/}" + local ext="$2" + local ff="$(echo "$filename_format" | + sed "s#{prefix}#$pref#;s#{name}#$name#")" + date +"${ff}${ext}" +} + +bk_run_command() { + # Run commands listed in entry. Disabled by default. + # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + # IT'S DANGEROUS! DON'T USE UNTRUSTED COMMANDS IN ENTRY! + # @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + if [[ "${allow_commands,,}" == "true" ]]; then + for cmd in "${entry_command[@]}"; do + bk_log -p "Executing command: $cmd" + # Run! + eval "$(printf "$cmd") &>> "$log"" + done + else + bk_log -p "ERROR: tried to execute commands: ${entry_command[@]}" + bk_err "Commands executing is not allowed!" + fi +} + +bk_do_archive() { + + _tar_exclude_items() { + # Exclude files and directories from backup. + for items in ${entry_exclude[*]}; do + items="$( tr ',' ' ' <<< "$items")" + for item in $items; do + echo -en "--exclude='$item' " + done + done + } + + for filepath in "${entry_archive[@]}"; do + + if [ -f "$filepath" ] || [ -d "$filepath" ]; then + : + else + bk_err "$filepath: no such file or directory." + fi + + echo "Archiving files: $filepath ..." | bk_log -p + + # Archive name. + dest_file="$entry_local"/"$(bk_get_name "$filepath" .tar.gz)" + + # Do archive! + eval \ + tar -czf "$dest_file" $(_tar_exclude_items) \ + -C $(dirname "$filepath") $(basename "$filepath") \ + |& bk_log -p + + bk_log -p "Archiving files: $filepath [Done]" + + # Verifying. + if [ ! "$no_verify" ]; then + if gunzip -c "$dest_file" | tar -t > /dev/null; then + bk_log -p "Integrity (gzip uncompressing and tar -t) [Success]" + else + bk_log -p "Integrity (gzip uncompressing and tar -t) [Fail]" + fi + + local in_dir="$(find "$filepath" | wc -l)" + local in_tar="$(tar -tf "$dest_file" | wc -l)" + + if [[ "$in_dir" == "$in_tar" ]]; then + bk_log -p \ + "Completeness: items: $in_dir, archived: $in_tar [Success]" + elif [[ "${#entry_exclude[@]}" != 0 ]]; then + echo "Completeness: Some files is excluded by configuration: + items: $in_dir, archived: $in_tar [Skipped]" \ + | sed 's/^ *//g' | tr '\n' ' ' | bk_log -p + else + bk_err \ + "Completeness: items: $in_dir, archived: $in_tar [Failed]" + fi + else + bk_log -p 'Archive integrity check is skipped.' + fi + + bk_log -p "Archive saved as: $dest_file" + bk_upload_file "$dest_file" + done +} + +bk_do_copy() { + # Copy files. + for filepath in "${entry_copy[@]}"; do + bk_log -p "Copying: $filepath" + if [ -f "$filepath" ]; then + dest_file="${entry_local}/$(bk_get_name "${filepath%%.*}" ".${filepath##*.}")" + elif [ -d "$filepath" ]; then + des_file="${entry_local}/$(bk_get_name "${filepath##*/}")" + fi + cp --archive "$filepath" "$dest_file" + bk_log -p "Copying: $filepath [Done]" + bk_upload_file "$dest_file" + done +} + +bk_dump_mysql() { + # Do MySQL dump. + + bk_log -p "Dumping database '${path##*/}' owned by ${user}@${host} (MySQL) ..." + + dump_name="$entry_local"/"$(bk_get_name "$path" .sql.gz )" + [ "$port" ] || port=3306 # Set default MySQL port. + + mysqldump \ + --host="$host" \ + --port="$port" \ + "${path##*/}" \ + --user="$user" \ + --password="$password" \ + | gzip -c > "$dump_name" |& bk_log + + if [ -s "$dump_name" ]; then + bk_log -p "Dumping database '${path##*/}' owned by ${user}@${host} (MySQL) [Done]" + bk_log -p "Dump saved as: $dump_name" + bk_upload_file "$dump_name" + else + rm "$dump_name" + bk_err "Something went wrong. Dump size is 0 bytes. Removing $dump_name" + fi +} + +bk_dump_postgresql() { + # Do PostgreSQL dump. + + bk_log "Dumping database '${path##*/}' owned by ${user}@${host} (PostgreSQL) ..." + + dump_name="$entry_local"/"$(bk_get_name "$db_name" .psql.gz)" + [ "$port" ] || port=5432 # Set default PostgreSQL port. + export PGPASSWORD="$password" + + pg_dump \ + --host="$host" \ + --port="$port" \ + --dbname="${path##*/}" \ + --username="$user" \ + --no-password | gzip -c > "$dump_name" |& bk_log + + unset PGPASSWORD + + if [ -s "$dump_name" ]; then + bk_log "Dumping database '${path##*/}' owned by ${user}@${host} (PostgreSQL) [Done]" + bk_log "Dump saved as: $dump_name" + bk_upload_file "$dump_name" + + else + rm "$dump_name" + bk_err "Something went wrong. Dump size is 0 bytes. Removing $dump_name" + fi +} + +bk_backup_database() { + # Do MariaDB/MySQL dump. + + for reqs in "${entry_database[@]}"; do + bk_parse_uri "$reqs" + case "$schema" in + mysql|mariadb) bk_dump_mysql;; + postgres|postgresql|psql) bk_dump_postgresql;; + esac + done +} + +########################################################################## +# RUN BACKUP +########################################################################## + +bk_find_entries() { + # Return array `all_entries` with entry files. + # from /etc/baka/entries (`entries`). Resolve symlinks too. + + # Find out entries. + while read -r file; do + # Resolve symlinks. + if [ -L "$file" ]; then + symb="$(readlink "$file")" + [ -f "$symb" ] && file="$symb" + fi + # Collect all entries, except ignored if set. + if [[ ! "${ignore[@]}" =~ "$(basename "$file")" ]] + then + all_entries+=("$file") + fi + done <<< "$(find "$entries" -type f)" + + # Exit if nothing found. + if [[ "${#all_entries[@]}" == 0 ]] + then + bk_err "$entries: No entries to backup." + fi + + # BEGIN Security issue fix. + # + # Check entries and baka.conf permissions. + for item in "${all_entries[@]}" "$conf"; do + if [ "$(stat -c "%a" "$item")" -gt 644 ]; then + bk_err "$item: Permissions are too open\n" \ + "It is recommended that your configurations" \ + "are NOT accessible by others." + fi + done + # END Security issue fix. +} + +bk_get_selected_entries() { + # Find entries passed as argument for `--entry` option. + for entry in "${sel_entries[@]}"; do + if [ -f "${PWD}/${entry}" ]; then + all_entries+=("${PWD}/${entry}") + elif [ -f "${entries}/${entry}" ]; then + all_entries+=("${entries}/${entry}") + elif [ -f "$entry" ]; then + all_entries+=("${entry}") + else + bk_log "baka: ERROR: $entry: Entry file not found" + bk_err "$entry: Entry file not found" + fi + done +} + +# Test + +bk_test_file() { + if [ -f "$1" ] || [ -d "$1" ]; then + : #do nothing, it's okay + else + bk_err "cannot backup: $1: No such file of directory" + fi +} + +bk_test_database() { + # $1 - database uri + + bk_parse_uri "$1" + + case "$schema" in + mysql|mariadb) + # MySQL checking. + [ "$port" ] || port=3306 # Set default MySQL port. + + echo -n "Checking database ${path##*/} on $host (MySQL): " + + echo 'SHOW VARIABLES LIKE "%version%";' | + mysql \ + --host="$host" \ + --port="$port" \ + "${path##*/}" \ + --user="$user" \ + --password="$password" > /dev/null + + echo "OK";; + postgres|postgresql|psql) + echo -n "Checking database: ${path##*/} on $host (PostgreSQL): " + + [ "$port" ] || port=5432 # Set default PostgreSQL port. + export PGPASSWORD="$password" + + echo 'SELECT version();' | + psql \ + --host="$host" \ + --port="$port" \ + --dbname="${path##*/}" \ + --username="$user" \ + --no-password > /dev/null + + echo "OK" + unset PGPASSWORD + ;; + *) bk_err "$schema: unsuported database type" + esac +} + +bk_test_remote() { + if [ "$entry_remote" == 'none' ]; then + echo "No remote storage." + else + echo "Remote server testing: "$entry_remote" ..." + echo 'This is baka test file' > ./testfile + echo "Test file uploading ..." + + bk_parse_uri "$entry_remote" + + case "$schema" in + rsync) + bk_upload_via_rsync ./testfile + echo "Test file uploaded successfully." + echo "Removing test file ..." + ssh -p "$port" "${user}@${host}" "rm -f ${path}/${dir_per_date}/testfile" + ;; + s3) + bk_upload_via_s3cmd ./testfile + echo "Test file uploaded successfully." + echo "Removing test file ..." + s3cmd -p --no-progress del "${entry_remote}/${dir_per_date}/testfile" + ;; + esac + rm ./testfile + fi + echo "Test is successful!" +} + +bk_get_local_dirs() { + # Return destination directories list. + + bk_find_entries # Get entries list. + + local temp=`mktemp` + + # Collect directories list + for entry in "${all_entries[@]}"; do + bk_load_entry "$entry" + bk_get_entry_local > /dev/null + echo "$entry_local" >> $temp + done + + # Save list. + local_dirs="$(cat $temp | sort -u)" + rm $temp +} + +bk_dir_per_date() { + [ "$dir_per_date" ] && dir_per_date="$(date +"$dir_per_date")" + entry_local="${entry_local}/${dir_per_date}" + if [ ! "$dry_run" ]; then + mkdir -p "$entry_local" + fi +} + +bk_do_backup() { + + ###################################################################### + # Main function for 'backup' command. + ###################################################################### + + # Parse args. + if [[ "$@" =~ -i|--i ]] && [[ "$@" =~ -e|--e ]] + then + bk_err \ + "You cannot use '--ignore' and '--entry' options together. + See 'baka backup help' or 'man baka' for more info." + fi + + while (( "$#" )); do + case "$1" in + -i|--ignore|--ignore=*) + bk_getopts "$1" "$2" + ignore+=("$arg") + shift "$sft";; + -e|--entry|--entry=*) + bk_getopts "$1" "$2" + sel_entries+=("$arg") + shift "$sft";; + --no-verify) no_verify=1; shift;; + --dry-run) dry_run=1; shift;; + --local) force_local=1; shift;; + --remove) remove_backups=1; shift;; + --help|help) bk_do_backup_help;; + *) bk_cli_err "$1"; exit 1;; + esac + done + + # Load baka.conf before actions. + bk_load_baka_conf + + # Startup log. + date +'Start: %d %b %Y %T %z' + bk_log "baka: Backup STARTED" + bk_log -p "Configuration file: $conf" + bk_log -p "Local storage:" \ + "$([ "$local" ] && echo "$local" || echo 'from entry')" + bk_log -p "Remote storage:" \ + "$([ "$remote" ] && echo "$remote" || echo 'from entry')" + + # Force local backup ('--local' option). + if [ "$force_local" ]; then + #remote='none' + bk_log -p "baka: WARNING: Local backup forced!" \ + "Backups will not be uploaded to remote storage." + fi + + # BEGIN Collect entries. + if [ "$sel_entries" ] # fetch from `--entry` option. + then + bk_get_selected_entries + # Selected entries basenames. + # Used only for display in echo below. + local seb="$( \ + for ent in ${all_entries[@]}; do + echo -n "$(basename $ent) " + done + )" + echo "Selected entries: $seb" | bk_log -p + else + # `--ignire` option. + [ "$ignore" ] && \ + echo "Skipped entries: ${ignore[@]}" | bk_log -p + bk_find_entries + fi + # END Collect entries. + + # Write to log entries list. + bk_log "Entries to backup (${#all_entries[@]}): ${all_entries[@]}" + + # Entry counter. + ecnt=${#all_entries[@]} # Entries count. + iter=1 # Iterator. + + # For every entry in `entries` ... + for entry in "${!all_entries[@]}"; do + # Display entry name and counter. + # Output: -> Entry: example.org [1/5] + echo + bk_log -p \ + "\e[1m-> Entry: ${all_entries[iter-1]##*/} [$(($entry+1))/$ecnt]\e[0m" + + # Get data. + bk_load_entry "${all_entries[iter-1]}" + bk_get_entry_local + + [ "$force_local" ] || bk_get_entry_remote + [ "$force_local" ] && entry_remote=none + + bk_get_entry_prefix "${all_entries[iter-1]}" + # Get entry_archive, entry_copy and others: + for variable in 'archive' 'copy' 'exclude' 'database' 'command'; do + bk_get_entry_data $variable + done + + # Backup BEGIN + + bk_dir_per_date + + bk_log -p "Path: ${all_entries[iter-1]}" + bk_log -p "Local storage: $entry_local" + bk_log -p "Remote storage: $entry_remote" + +#echo "RECOVERED LOCAL: ${entry_local//$dir_per_date/}" + + [ "$dry_run" ] || { + bk_run_command + bk_do_archive + bk_do_copy + bk_backup_database + } + + [ "$dry_run" ] && { + bk_test_remote + } + + # Backup END + + let iter++ # Increase iterator for entry counter. + done + + # Remove old backups ('--remove' option). + if [ "$remove_backups" ]; then + echo + bk_log -p "\e[1m-> Remove old backups ...\e[0m" + bk_remove_old_backups --force + fi + + echo # Just print new line. + bk_log "baka: Backup FINISHED" + echo "Backup [Done]" +} + +# LIST + +bk_list_entries() { + # List entries. + + while (( "$#" )); do + case "$1" in + -v|--verbose) _verbose=1; shift;; + --help|help) bk_list_entries_help;; + *) bk_cli_err "$1"; exit 1;; + esac + done + + # Load baka.conf before actions. + bk_load_baka_conf + + bk_find_entries # Get entries list. + + if [ "$_verbose" ]; then + # Verbosely. + { + echo 'Name | Path | Local |Remote' + for entry in "${all_entries[@]}"; do + echo -en "${entry##*/} | $entry" + bk_load_entry "$entry" + bk_get_entry_local > /dev/null + bk_get_entry_remote > /dev/null + echo -en "| $entry_local |" + echo -e "$entry_remote" + done + } | column -t -s '|' + #| column --table --table-columns Name,Path,Local,Remote --separator '|' + else + # Short format. + for entry in "${all_entries[@]}"; do + echo -e "$entry" + done + fi +} + +# REMOVE + +bk_remove_old_backups() { + # Remove old backups from local storage. + + while (( "$#" )); do + case "$1" in + -l|--list) remove_list=1; shift;; + -f|--force) assume_yes=1; shift;; + --help|help) + bk_remove_old_backups_help;; + *) bk_cli_err "$1"; exit 1;; + esac + done + + # Load baka.conf before actions. + bk_load_baka_conf + + bk_get_local_dirs + [ "$remove_list" ] && { + while read -r dir; do + find "$dir" -type f -mtime +$livetime + done <<< "$local_dirs" + exit 0 + } + + _yesno() { + # Yes/No interactive dialog. + # + # Usage: if yesno 'Question'; then ... + + local __answer= + [ "$assume_yes" ] && return 0 + + while [ ! "$__answer" ]; do + echo -en "$1 [y/n] " + read -r reply + case "${reply,,}" in + y|yes) __answer=0;; + n|no) __answer=1;; + *) echo "Please, answer y or n";; + esac + done + return "$__answer" + } + + if [ "$assume_yes" ]; then + bk_log -p "baka: WARNING: Forced deletion of old backups is enabled." + else + echo \ + "Files older than $livetime days will be removed. This action cannot be undone." + echo -e "Use '--force' option to remove files without prompt.\n" + if _yesno "Continue?"; then + : + else + echo Aborted.; exit 130 + fi + fi + + while read -r dir; do + bk_log "Removing old backups (older than $livetime days) ..." + bk_log "Removing files from directory: $dir" + find "$dir" -type f -mtime +$livetime -delete -print | bk_log + echo '(empty output -- no files deleted)' + done <<< "$local_dirs" +} + +########################################################################## +# CLI ARGUMENTS PARSING +########################################################################## + +bk_getopts() { + # GNU-style CLI options parser. + # + # Parse --opt VAL and --opt=VAL options. + # Requires 2 arguments: $1, $2. + # Returns: + # $opt - option name. + # $arg - option's value. + # $sft - value for shift. + if [[ "$1" =~ .+=.+ ]]; then + opt="${1%%=*}"; arg="${1#*=}"; sft=1 + elif [[ ! "$1" =~ .+=$ ]] && \ + [ "$2" ] && [ "${2:0:1}" != '-' ] + then + opt="$1"; arg="$2"; sft=2 + else + opt="$1" + if [[ "$1" =~ .+=$ ]]; then opt="${1:0: -1}"; fi + bk_err -s "missing argument for: $opt" + fi +} + +# Show help if no arguments is passed. +[[ "$@" ]] || bk_help + +# MAIN ARGS PARSER +################## + +while (( "$#" )); do + case "$1" in + backup) shift; bk_do_backup "$@"; shift "$#";; + list) shift; bk_list_entries "$@"; shift "$#";; + remove) shift; bk_remove_old_backups "$@"; shift "$#";; + -c|--config|--config=*) + bk_getopts "$1" "$2"; conf="$arg"; shift "$sft" + [ -f "${PWD}/${conf}" ] && conf="${PWD}/${conf}";; + --version) bk_version;; + --help|help) bk_help "$2";; + *) bk_cli_err "$1"; exit 1 + esac +done diff --git a/completion b/src/completion similarity index 64% rename from completion rename to src/completion index a8c90aa..6a2c451 100755 --- a/completion +++ b/src/completion @@ -1,16 +1,16 @@ #!/usr/bin/env bash -# baka completion script. +# * baka (v 0.2.0) completion script. -baka_get_entries() { +_baka_get_entries() { # Collect entries list from /etc/baka/entries - baka_entries=./entries # /etc/baka/entries + + baka_entries=/etc/baka/entries # This is just baka bk_find_entries() function copy-paste. local all_files="$(find "$baka_entries" -type f)" - for file in $all_files - do + for file in $all_files; do # Resolve symlinks. if [ -L "$file" ]; then s="$(readlink "$file")" @@ -19,47 +19,44 @@ baka_get_entries() { fi fi # Collect all entries, except ignored if set. - if [[ ! "${ignore[@]}" =~ "$(basename $file)" ]] - then - bk_all_entries+=("${file##*/}") + if [[ ! "${ignore[@]}" =~ "$(basename $file)" ]]; then + all_entries+=("${file##*/}") fi done - echo "${bk_all_entries[@]}" + echo "${all_entries[@]}" } -baka_completion() { +_baka() { local cur prev cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} case ${COMP_CWORD} in - 1) + 1|3) # Firs level commands. COMPREPLY=($(compgen -W \ - "help --help --version - backup list test show edit remove" -- ${cur})) + "help --help --version -c --config + backup list remove" -- ${cur})) ;; - 2) + 2|4) # Subcommands. case ${prev} in backup) COMPREPLY=($(compgen -W \ "help --help -i --ignore -e --entry - --local --no-verify --remove" -- ${cur})) + --local --no-verify --dry-run --remove" -- ${cur})) ;; list) COMPREPLY=($(compgen -W \ - "help --help - --verbose -v --short -s -S" -- ${cur})) - ;; - test) - COMPREPLY=($(compgen -W \ - "help --help --verbose -v" -- ${cur})) + "help --help -v --verbose" -- ${cur})) ;; remove) COMPREPLY=($(compgen -W \ - "help --help --force -f" -- ${cur})) + "help --help -f --force -l --list" -- ${cur})) + ;; + -c|--config|--config=) + compopt -o default; COMPREPLY=() ;; *) COMPREPLY=() ;; @@ -69,7 +66,7 @@ baka_completion() { # Subcommand options completion. case ${COMP_WORDS[2]} in -i|--ignore|--ignore=|-e|--entry|--entry=) - entries_list="$(baka_get_entries)" + entries_list="$(_baka_get_entries)" COMPREPLY=($(compgen -W \ "$entries_list" -- ${cur})) ;; @@ -84,4 +81,4 @@ baka_completion() { esac } -complete -F baka_completion baka +complete -F _baka baka ./baka