diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index dd0aba6..3702562 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -44,6 +44,8 @@ jobs: - test-local-no_perms - test-local-no_times - test-local-copy_links + - test-local-crazy-filename-chars + - test-local-crazy-pathname-chars name: "[Linux ${{ matrix.target }}]" steps: diff --git a/.github/workflows/macos-10.yml b/.github/workflows/macos-10.yml new file mode 100644 index 0000000..63a72a8 --- /dev/null +++ b/.github/workflows/macos-10.yml @@ -0,0 +1,105 @@ +--- + +# ------------------------------------------------------------------------------------------------- +# Job Name +# ------------------------------------------------------------------------------------------------- +name: MacOS-10 + + +# ------------------------------------------------------------------------------------------------- +# When to run +# ------------------------------------------------------------------------------------------------- +on: + # Runs on Pull Requests + pull_request: + + # Runs on master Branch and Tags + push: + branches: + - master + tags: + - '[0-9]+.[0-9]+*' + + +# ------------------------------------------------------------------------------------------------- +# What to run +# ------------------------------------------------------------------------------------------------- +jobs: + macos-10: + + runs-on: macos-10.15 + + strategy: + fail-fast: false + matrix: + target: + - test-local-default-abs-noslash-noslash + - test-local-default-abs-noslash-slash + - test-local-default-abs-slash-noslash + - test-local-default-abs-slash-slash + - test-local-default-rel-noslash-noslash + - test-local-default-rel-noslash-slash + - test-local-default-rel-slash-noslash + - test-local-default-rel-slash-slash + - test-local-no_perms + - test-local-no_times + - test-local-copy_links + - test-local-crazy-filename-chars + - test-local-crazy-pathname-chars + + name: "[MacOS ${{ matrix.target }}]" + steps: + + # ------------------------------------------------------------ + # Checkout repository + # ------------------------------------------------------------ + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Show environment + shell: bash + run: | + env + + - name: Show user + shell: bash + run: | + id + + - name: Show bash versions + shell: bash + run: | + bash --version + + - name: Show rsync versions + shell: bash + run: | + rsync --version + + # ------------------------------------------------------------ + # Install + # ------------------------------------------------------------ + - name: Install + shell: bash + run: | + sudo make install + /usr/local/bin/timemachine --version + + # ------------------------------------------------------------ + # Test + # ------------------------------------------------------------ + - name: Test + shell: bash + run: | + make ${TARGET} + env: + TARGET: ${{ matrix.target }} + + # ------------------------------------------------------------ + # Uninstall + # ------------------------------------------------------------ + - name: Uninstall + shell: bash + run: | + sudo make uninstall + if test -f /usr/local/bin/timemachine >/dev/null 2>&1; then false; fi diff --git a/.github/workflows/macos.yml b/.github/workflows/macos-11.yml similarity index 95% rename from .github/workflows/macos.yml rename to .github/workflows/macos-11.yml index c989e63..54daeb4 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos-11.yml @@ -3,7 +3,7 @@ # ------------------------------------------------------------------------------------------------- # Job Name # ------------------------------------------------------------------------------------------------- -name: MacOS +name: MacOS-11 # ------------------------------------------------------------------------------------------------- @@ -25,9 +25,9 @@ on: # What to run # ------------------------------------------------------------------------------------------------- jobs: - macos: + macos-11: - runs-on: macos-latest + runs-on: macos-11 strategy: fail-fast: false @@ -44,6 +44,8 @@ jobs: - test-local-no_perms - test-local-no_times - test-local-copy_links + - test-local-crazy-filename-chars + - test-local-crazy-pathname-chars name: "[MacOS ${{ matrix.target }}]" steps: diff --git a/.github/workflows/ssh.yml b/.github/workflows/ssh.yml index a6deffe..24590cd 100644 --- a/.github/workflows/ssh.yml +++ b/.github/workflows/ssh.yml @@ -44,6 +44,7 @@ jobs: - test-remote-ssh_config-default - test-remote-ssh_config-port_1111 - test-remote-ssh_config-port_overwrite + - test-remote-ssh_crazy-source-pathname-chars name: "[SSH ${{ matrix.target }}]" steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e8b5d0..ec6966c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog +## Release v1.3.1 + +#### Changed +- Switch to semver for versioning +- Wrap rsync into eval to escape paths + +#### Added +- CI: check for special chars in file names + +#### Fixed +- Fixed #69 special chars in source directory +- Fixed #67 argument parsing + + ## Release v1.2 #### Fixed diff --git a/LICENSE.md b/LICENSE.md old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile index a8f6769..4703ba5 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,8 @@ test: test-local-default-rel-slash-slash test: test-local-no_perms test: test-local-no_times test: test-local-copy_links +test: test-local-crazy-filename-chars +test: test-local-crazy-pathname-chars test: test-remote-default-abs test: test-remote-default-rel test: test-remote-ssh_1111_port-nouser @@ -102,7 +104,7 @@ test: test-remote-ssh_def_port-user test: test-remote-ssh_config-default test: test-remote-ssh_config-port_1111 test: test-remote-ssh_config-port_overwrite - +test: test-remote-ssh_crazy-source-pathname-chars test-local-default-abs-noslash-noslash: ./tests/01-run-local-default-abs-noslash-noslash.sh @@ -137,6 +139,11 @@ test-local-no_times: test-local-copy_links: ./tests/05-run-local-copy_links.sh +test-local-crazy-filename-chars: + ./tests/06-run-local-crazy-filename-chars.sh + +test-local-crazy-pathname-chars: + ./tests/06-run-local-crazy-pathname-chars.sh test-remote-default-abs: ./tests/10-run-remote-default-abs.sh @@ -171,6 +178,9 @@ test-remote-ssh_config-port_1111: test-remote-ssh_config-port_overwrite: ./tests/12-run-remote-ssh_config-port_overwrite.sh +test-remote-ssh_crazy-source-pathname-chars: + ./tests/13-run-remote-crazy-source-pathname-chars.sh + # ------------------------------------------------------------------------------------------------- # Helper targets diff --git a/README.md b/README.md index aa0619a..711c339 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ [![Linting](https://github.com/cytopia/linux-timemachine/workflows/Linting/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=Linting) [![Linux](https://github.com/cytopia/linux-timemachine/workflows/Linux/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=Linux) -[![MacOS](https://github.com/cytopia/linux-timemachine/workflows/MacOS/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS) +[![MacOS 10](https://github.com/cytopia/linux-timemachine/workflows/MacOS-10/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS-10) +[![MacOS 11](https://github.com/cytopia/linux-timemachine/workflows/MacOS-11/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS-11) [![SSH](https://github.com/cytopia/linux-timemachine/workflows/SSH/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=SSH) [![Tag](https://img.shields.io/github/tag/cytopia/linux-timemachine.svg)](https://github.com/cytopia/linux-timemachine/releases) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) @@ -198,7 +199,8 @@ Retention is a delicate topic as you want to be sure that data is removed as int [![Linting](https://github.com/cytopia/linux-timemachine/workflows/Linting/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=Linting) [![Linux](https://github.com/cytopia/linux-timemachine/workflows/Linux/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=Linux) -[![MacOS](https://github.com/cytopia/linux-timemachine/workflows/MacOS/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS) +[![MacOS 10](https://github.com/cytopia/linux-timemachine/workflows/MacOS-10/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS-10) +[![MacOS 11](https://github.com/cytopia/linux-timemachine/workflows/MacOS-11/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=MacOS-11) [![SSH](https://github.com/cytopia/linux-timemachine/workflows/SSH/badge.svg)](https://github.com/cytopia/linux-timemachine/actions?workflow=SSH) The script is written and maintained with maximum care. diff --git a/tests/.lib/create-source.sh b/tests/.lib/create-source.sh index 490e4e2..1ac2fdb 100644 --- a/tests/.lib/create-source.sh +++ b/tests/.lib/create-source.sh @@ -15,22 +15,35 @@ set -o pipefail create_file() { local src_dir="${1}" local file_name="${2}" + local sub_dir= + local file_path= local file_size="${3}" local file_perms="${4}" - local file_path="${src_dir}/${file_name}" + file_name="$( printf "%q" "${file_name}" )" + sub_dir="$( printf "%q" "$( eval "dirname ${file_name}" )" )" + file_path="${src_dir}/${file_name}" - # Create directory if it doesn't exist - if [ ! -d "$( dirname "${file_path}" )" ]; then - run "mkdir -p $( dirname "${file_path}" )" + if ! eval "test -d ${src_dir}"; then + printf "No such directpry: %s\\n" "${src_dir}" + return 1 + fi + # Create sub-directory if it doesn't exist + if [ "${sub_dir}" != "." ]; then + printf "# Create basedir: %s\\n" "${sub_dir}" + run "mkdir -p ${src_dir}/${sub_dir}" fi - if [ "$(uname)" = "Linux" ]; then run "dd if=/dev/zero of=${file_path} bs=1M count=${file_size} 2>/dev/null" else run "dd if=/dev/zero of=${file_path} bs=1m count=${file_size} 2>/dev/null" fi - run "chmod ${file_perms} '${file_path}'" + run "chmod ${file_perms} ${file_path}" + + if ! eval "test -f ${file_path}"; then + echo "No file created: ${file_path}" + return 1 + fi } @@ -40,13 +53,20 @@ create_file() { create_link() { local src_dir="${1}" local link_name="${2}" + local sub_dir= local link_from="${3}" - local link_path="${src_dir}/${link_name}" - # Create directory if it doesn't exist - if [ ! -d "$( dirname "${link_path}" )" ]; then - run "mkdir -p $( dirname "${link_path}" )" - fi + link_name="$( printf "%q" "${link_name}" )" + sub_dir="$( printf "%q" "$( eval "dirname ${link_name}" )" )" - run "cd '${src_dir}' && ln -s '${link_from}' '${link_name}'" + if ! eval "test -d ${src_dir}"; then + printf "No such directpry: %s\\n" "${src_dir}" + return 1 + fi + # Create sub-directory if it doesn't exist + if [ "${sub_dir}" != "." ]; then + printf "# Create basedir: %s\\n" "${sub_dir}" + run "mkdir -p ${src_dir}/${sub_dir}" + fi + run "cd ${src_dir} && ln -s ${link_from} ${link_name}" } diff --git a/tests/.lib/dir-size.sh b/tests/.lib/dir-size.sh index c1cb673..1113807 100644 --- a/tests/.lib/dir-size.sh +++ b/tests/.lib/dir-size.sh @@ -10,8 +10,10 @@ set -o pipefail # ------------------------------------------------------------------------------------------------- check_dir_size() { - local src="${1}" - local dst="${2}" + local src= + local dst= + src="$( printf "%q" "${1}" )" + dst="$( printf "%q" "${2}" )" src_size="$( get_dir_size_with_hardlinks "${src}" )" dst_size="$( get_dir_size_with_hardlinks "${dst}" )" @@ -21,7 +23,7 @@ check_dir_size() { return 0 fi printf "[TEST] [FAIL] src-dir(%s) and dst-dir(%s) size don't match: (src: %s) (dst: %s)\\r\\n" "${src_size}" "${dst_size}" "${src}" "${dst}" - exit 1 + return 1 } @@ -36,9 +38,9 @@ check_dir_size() { ### @param abs_path directory ### get_dir_size_with_hardlinks() { - local dir="${1}" + local dir= local size= - + dir="$( printf "%q" "${1}" )" size="$( run "cd '${dir}' && du -d0 '.' | awk '{print \$1}'" "1" "stderr" )" echo "${size}" @@ -51,23 +53,25 @@ get_dir_size_with_hardlinks() { ### @param abs_path directory ### get_dir_size_without_hardlinks() { - local dir="${1}" + local dir= + dir="$( printf "%q" "${1}" )" local suffix="${2:-}" local actual_path= local current_dir_name= local parent_dir_path= local size= + # Return the actual path (in case we're in a symlink) - actual_path="$( run "cd '${dir}' && pwd -P" "1" "stderr" )" + actual_path="$( printf "%q" "$( run "cd ${dir} && pwd -P" "1" "stderr" )" )" # Get only the name of the current directory - current_dir_name="$( run "basename '${actual_path}'" "1" "stderr" )" + current_dir_name="$( printf "%q" "$( run "basename ${actual_path}" "1" "stderr" )" )" # Get the parent directory path - parent_dir_path="$( run "dirname '${actual_path}'" "1" "stderr" )" + parent_dir_path="$( printf "%q" "$( run "dirname ${actual_path}" "1" "stderr" )" )" - size="$( run "cd '${parent_dir_path}' && du -d2 2>/dev/null | grep -E '${current_dir_name}${suffix}\$' | head -1 | awk '{print \$1}'" "1" "stderr" )" + size="$( run "cd ${parent_dir_path} && du -d2 2>/dev/null | grep -E '${current_dir_name}${suffix}\$' | head -1 | awk '{print \$1}'" "1" "stderr" )" echo "${size}" } diff --git a/tests/.lib/file-exist.sh b/tests/.lib/file-exist.sh index 33e5fa7..8ca0a69 100644 --- a/tests/.lib/file-exist.sh +++ b/tests/.lib/file-exist.sh @@ -20,14 +20,26 @@ check_src_dst_file_exist() { local f="${1}" local src_dir="${2}" local dst_dir="${3}" + local src= + local dst= - if [ ! -f "${src_dir}/${f}" ]; then - printf "[TEST] [FAIL] Source file does not exist: %s\\r\\n" "${src_dir}/${f}" - exit 1 + file_name="$( printf "%q" "${f}" )" + src="${src_dir}/${file_name}" + dst="${dst_dir}/${file_name}" + + + cmd="test -f ${src}" + if ! eval "${cmd}"; then + printf "[TEST] [FAIL] Source file does not exist: %s\\r\\n" "${src}" + printf "%s" "$( ls "${src_dir}" )" + return 1 fi - if [ ! -f "${dst_dir}/${f}" ]; then - printf "[TEST] [FAIL] Destination file does not exist: %s\\r\\n" "${dst_dir}/${f}" - exit 1 + + cmd="test -f ${dst}" + if ! eval "${cmd}"; then + printf "[TEST] [FAIL] Destination file does not exist: %s\\r\\n" "${dst}" + printf "%s" "$( ls "${src_dir}" )" + return 1 fi printf "[TEST] [OK] Source and Destination files exist\\r\\n" } @@ -42,16 +54,31 @@ check_src_dst_file_exist() { check_dst_file_is_file() { local f="${1}" local dst_dir="${2}" + local dst= + + file_name="$( printf "%q" "${f}" )" + dst="${dst_dir}/${file_name}" + + cmd="test -d ${dst}" + if eval "${cmd}"; then + printf "[TEST] [FAIL] Destination file is a directory: %s\\r\\n" "${dst}" + return 1 + fi - if [ -d "${dst_dir}/${f}" ]; then - printf "[TEST] [FAIL] Destination file is a directory: %s\\r\\n" "${dst_dir}/${f}" - exit 1 + cmd="test -L ${dst}" + if eval "${cmd}"; then + printf "[TEST] [FAIL] Destination file is a symlink: %s\\r\\n" "${dst}" + return 1 fi - if [ -L "${dst_dir}/${f}" ]; then - printf "[TEST] [FAIL] Destination file is a symlink: %s\\r\\n" "${dst_dir}/${f}" - exit 1 + + cmd="test -f ${dst}" + if eval "${cmd}"; then + printf "[TEST] [OK] Destination file is a regular file: %s\\r\\n" "${dst}" + return 0 fi - printf "[TEST] [OK] Destination file is a regular file\\r\\n" + + printf "[TEST] [FAIL] Destination file is not a regular file: %s\\r\\n" "${dst}" + return 1 } @@ -64,17 +91,20 @@ check_dst_file_is_file() { check_dst_file_is_link() { local f="${1}" local dst_dir="${2}" + local dst= - if [ -d "${dst_dir}/${f}" ]; then - printf "[TEST] [FAIL] Destination file is a directory: %s\\r\\n" "${dst_dir}/${f}" - exit 1 + dst="$( printf "%q" "${dst_dir}/${f}" )" + + if [ -d "${dst}" ]; then + printf "[TEST] [FAIL] Destination file is a directory: %s\\r\\n" "${dst}" + return 1 fi - if [ -L "${dst_dir}/${f}" ]; then + if [ -L "${dst}" ]; then printf "[TEST] [OK] Destination file is a symlink\\r\\n" return fi - printf "[TEST] [FAIL] Destination file is not a symlink: %s\\r\\n" "${dst_dir}/${f}" - exit 1 + printf "[TEST] [FAIL] Destination file is not a symlink: %s\\r\\n" "${dst}" + return 1 } @@ -89,10 +119,15 @@ check_src_dst_file_equal() { local f="${1}" local src_dir="${2}" local dst_dir="${3}" + local src= + local dst= + + src="${src_dir}/$( printf "%q" "${f}" )" + dst="${dst_dir}/$( printf "%q" "${f}" )" - if ! run "cmp -s '${src_dir}/${f}' '${dst_dir}/${f}'"; then - printf "[TEST] [FAIL] Source (%s) and dest (%s) files differ\\r\\n" "${src_dir}/${f}" "${dst_dir}/${f}" - exit 1 + if ! run "cmp ${src} ${dst}"; then + printf "[TEST] [FAIL] Source (%s) and dest (%s) files differ\\r\\n" "${src}" "${dst}" + return 1 else printf "[TEST] [OK] Source and dest files are equal\\r\\n" fi diff --git a/tests/.lib/file-owner.sh b/tests/.lib/file-owner.sh index f38b683..56f01f9 100644 --- a/tests/.lib/file-owner.sh +++ b/tests/.lib/file-owner.sh @@ -28,7 +28,7 @@ check_src_dst_file_uid() { if [ "${src_uid}" != "${dst_uid}" ]; then printf "[TEST] [FAIL] Owner uid: (%s) src and dst don't match: %s != %s\\r\\n" "${f}" "${src_uid}" "${dst_uid}" - exit 1 + return 1 else printf "[TEST] [OK] Owner uid: (%s) src and dst match: %s = %s\\r\\n" "${f}" "${src_uid}" "${dst_uid}" fi @@ -54,7 +54,7 @@ check_src_dst_file_gid() { if [ "${src_gid}" != "${dst_gid}" ]; then printf "[TEST] [FAIL] Owner gid: (%s) src and dst don't match: %s != %s\\r\\n" "${f}" "${src_gid}" "${dst_gid}" - exit 1 + return 1 else printf "[TEST] [OK] Owner gid: (%s) src and dst match: %s = %s\\r\\n" "${f}" "${src_gid}" "${dst_gid}" fi diff --git a/tests/.lib/file-permissions.sh b/tests/.lib/file-permissions.sh index cee508f..2fcf873 100644 --- a/tests/.lib/file-permissions.sh +++ b/tests/.lib/file-permissions.sh @@ -28,7 +28,7 @@ check_dst_file_perm() { if [ "${exp_perm}" != "${dst_perm}" ]; then printf "[TEST] [FAIL] Permissions: %s: (src: %s) (exp: %s) (dst: %s}\\r\\n" "${f}" "${src_perm}" "${exp_perm}" "${dst_perm}" - exit 1 + return 1 else printf "[TEST] [OK] Permissions: %s: (src: %s) (exp: %s) (dst: %s}\\r\\n" "${f}" "${src_perm}" "${exp_perm}" "${dst_perm}" fi @@ -54,7 +54,7 @@ check_src_dst_file_perm() { if [ "${src_perm}" != "${dst_perm}" ]; then printf "[TEST] [FAIL] Permissions: (%s) src and dst don't match: %s != %s\\r\\n" "${f}" "${src_perm}" "${dst_perm}" - exit 1 + return 1 else printf "[TEST] [OK] Permissions: (%s) src and dst match: %s = %s\\r\\n" "${f}" "${src_perm}" "${dst_perm}" fi @@ -182,11 +182,12 @@ get_default_dest_file_perm() { get_file_perm() { local file_path="${1}" local out + file_path="$( printf "%q" "${file_path}" )" if [ "$(uname)" = "Linux" ]; then - out="$( run "stat -c '%a' '${file_path}'" "1" "stderr" )" + out="$( run "stat -c '%a' ${file_path}" "1" "stderr" )" else - out="$( run "stat -f '%A' '${file_path}'" "1" "stderr" )" + out="$( run "stat -f '%A' ${file_path}" "1" "stderr" )" fi out="${out//\"/}" >&2 echo "${out}" diff --git a/tests/.lib/file-size.sh b/tests/.lib/file-size.sh index 2636682..625ebb9 100644 --- a/tests/.lib/file-size.sh +++ b/tests/.lib/file-size.sh @@ -28,7 +28,7 @@ check_src_dst_file_size() { if [ "${src_size}" != "${dst_size}" ]; then printf "[TEST] [FAIL] File size: (%s) src and dst don't match: %s != %s\\r\\n" "${f}" "${src_size}" "${dst_size}" - exit 1 + return 1 else printf "[TEST] [OK] File size: (%s) src and dst match: %s = %s\\r\\n" "${f}" "${src_size}" "${dst_size}" fi diff --git a/tests/.lib/file-time.sh b/tests/.lib/file-time.sh index 43c9401..bb5c3e0 100644 --- a/tests/.lib/file-time.sh +++ b/tests/.lib/file-time.sh @@ -36,7 +36,7 @@ check_src_dst_file_mod_time() { if [ "${match}" = "0" ]; then if [ "${src_time}" = "${dst_time}" ]; then printf "[TEST] [FAIL] Mod time: (%s) src and dst match: %s != %s\\r\\n" "${f}" "${src_time}" "${dst_time}" - exit 1 + return 1 else printf "[TEST] [OK] Mod time: (%s) src and dst do not match: %s = %s\\r\\n" "${f}" "${src_time}" "${dst_time}" fi @@ -44,7 +44,7 @@ check_src_dst_file_mod_time() { else if [ "${src_time}" != "${dst_time}" ]; then printf "[TEST] [FAIL] Mod time: (%s) src and dst don't match: %s != %s\\r\\n" "${f}" "${src_time}" "${dst_time}" - exit 1 + return 1 else printf "[TEST] [OK] Mod time: (%s) src and dst match: %s = %s\\r\\n" "${f}" "${src_time}" "${dst_time}" fi diff --git a/tests/.lib/functions.sh b/tests/.lib/functions.sh index 20e05dc..7ce1eec 100644 --- a/tests/.lib/functions.sh +++ b/tests/.lib/functions.sh @@ -113,7 +113,8 @@ create_tmp_file() { tmp_file="$( mktemp )" fi - printf "%s" "${tmp_file}" | sed 's|/*$||' + echo "${tmp_file}" + #printf "%s" "${tmp_file}" | sed 's|/*$||' } @@ -123,6 +124,7 @@ create_tmp_file() { create_tmp_dir() { local absolute="${1:-1}" local pwd="${2:-}" + local suffix="${3:-}" local tmp_dir= ### @@ -130,11 +132,11 @@ create_tmp_dir() { ### if [ "${absolute}" = "0" ]; then i=0 - while [ -d "${pwd}/.tmp/${i}" ] || [ -f "${pwd}/.tmp/${i}" ]; do + while [ -d "${pwd}/.tmp/${i}${suffix}" ] || [ -f "${pwd}/.tmp/${i}${suffix}" ]; do i="$(( i + 1 ))" done - tmp_dir=".tmp/${i}" - run "cd '${pwd}' && mkdir -p '${tmp_dir}'" "1" "stderr" "stderr" + tmp_dir=".tmp/${i}${suffix}" + run "cd ${pwd} && mkdir -p ${tmp_dir}" "1" "stderr" "stderr" echo "${tmp_dir}" return fi @@ -142,17 +144,13 @@ create_tmp_dir() { ### ### Create absolute path tmp dir ### - if ! command -v mktemp >/dev/null 2>&1; then - i=0 - local prefix="/tmp/timemachine" - while [ -d "${prefix}-${i}" ] || [ -f "${prefix}-${i}" ]; do - i="$(( i + 1 ))" - done - tmp_dir="${prefix}-${i}" - mkdir -p "${tmp_dir}" >/dev/null - else - tmp_dir="$( mktemp -d )" - fi - - printf "%s" "${tmp_dir}" | sed 's|/*$||' + # NOTE: MacOS does not have 'mktemp --suffix', so we're using our own version + i=0 + local prefix="/tmp/timemachine" + while [ -d "${prefix}-${i}${suffix}" ] || [ -f "${prefix}-${i}${suffix}" ]; do + i="$(( i + 1 ))" + done + tmp_dir="$( printf "%q" "${prefix}-${i}${suffix}" )" + run "mkdir -p ${tmp_dir}" "1" "stderr" "stderr" + echo "${tmp_dir}" } diff --git a/tests/.lib/run-backup.sh b/tests/.lib/run-backup.sh index cd9df56..6a30048 100644 --- a/tests/.lib/run-backup.sh +++ b/tests/.lib/run-backup.sh @@ -23,6 +23,9 @@ run_backup() { local out local err + rsync_args="${rsync_args} --progress --verbose" + timemachine_path="$( printf "%q" "${timemachine_path}" )" + out="$( create_tmp_file )" err="$( create_tmp_file )" @@ -34,13 +37,13 @@ run_backup() { ### ### Run and check for failure ### - if ! run "cd \"${pwd}\" && \"${timemachine_path}\" -d \"${src_dir}\" \"${dst_dir}\" ${rsync_args} > \"${out}\" 2> \"${err}\""; then + if ! run "cd \"${pwd}\" && ${timemachine_path} -d ${src_dir} ${dst_dir} ${rsync_args} > \"${out}\" 2> \"${err}\""; then printf "[TEST] [FAIL] Run failed.\\r\\n" cat "${out}" cat "${err}" rm "${out}" rm "${err}" - exit 1 + return 1 fi cat "${out}" echo @@ -53,7 +56,7 @@ run_backup() { printf "Warnings:\\r\\n----------\\r\\n%s\\r\\n" "$( cat "${err}" )" rm "${out}" rm "${err}" - exit 1 + return 1 fi printf "[TEST] [OK] No warnings detected.\\r\\n" @@ -64,29 +67,29 @@ run_backup() { printf "[TEST] [FAIL] Not a '%s' backup\\r\\n" "${backup_type}" rm "${out}" rm "${err}" - exit 1 + return 1 fi printf "[TEST] [OK] Backup type: '%s' backup.\\r\\n" "${backup_type}" ### ### Check for existing latest symlink ### - if [ ! -L "${pwd}/${dst_dir}/current" ]; then + if ! eval "test -L ${pwd}/${dst_dir}/current"; then printf "[TEST] [FAIL] No latest symlink available: %s\\r\\n" "${pwd}/${dst_dir}/current" rm "${out}" rm "${err}" - exit 1 + return 1 fi printf "[TEST] [OK] Latest symlink available: %s\\r\\n" "${pwd}/${dst_dir}/current" ### ### Check partial backup .inprogress directory ### - if [ -d "${pwd}/${dst_dir}/current/.inprogress" ]; then + if eval "test -d ${pwd}/${dst_dir}/current/.inprogress"; then printf "[TEST] [FAIL] Undeleted '.inprogress' directory found: %s\\r\\n" "${pwd}/${dst_dir}/current/.inprogress" rm "${out}" rm "${err}" - exit 1 + return 1 fi printf "[TEST] [OK] No '.inprogress' directory found\\r\\n" @@ -118,16 +121,32 @@ run_remote_backup() { out="$( create_tmp_file )" err="$( create_tmp_file )" + exc="$( create_tmp_file )" ### ### Give 2 seconds time for a new unique directory name (second based) to be created ### sleep 2 + # Create execution file + { + printf "#!/bin/bash\\n"; + printf "SRC='%s'\\n" "${src_dir}"; + printf "DST='%s'\\n" "${dst_dir}"; + printf "BIN=\"%s\"\\n" "${timemachine_path}"; + printf "ARG=\"%s\"\\n" "${timemachine_args}"; + printf "SSH=\"%s\"\\n" "${ssh_string}"; + printf "RAR=\"%s\"\\n" "${rsync_args}"; + printf "bash \${BIN} -d \${ARG} \"\${SRC}\" \${SSH}:\"\${DST}\" \${RAR}\\n"; + } > "${exc}" + chmod +x "${exc}" + docker cp "${exc}" "${docker_client_name}:/run.sh" + #cat "${exc}" ### ### Run and check for failure ### - if ! run "docker exec ${docker_client_name} ${timemachine_path} -d ${timemachine_args} ${src_dir} ${ssh_string}:${dst_dir} ${rsync_args} > \"${out}\" 2> \"${err}\""; then + if ! run "docker exec ${docker_client_name} /run.sh > \"${out}\" 2> \"${err}\""; then + #if ! run "docker exec ${docker_client_name} ${timemachine_path} -d ${timemachine_args} ${src_dir} ${ssh_string}:${dst_dir} ${rsync_args} > \"${out}\" 2> \"${err}\""; then printf "[TEST] [FAIL] Run failed.\\r\\n" cat "${out}" cat "${err}" @@ -135,7 +154,7 @@ run_remote_backup() { rm "${err}" run "docker rm -f ${docker_client_name}" || true run "docker rm -f ${docker_server_name}" || true - exit 1 + return 1 fi cat "${out}" echo @@ -150,7 +169,7 @@ run_remote_backup() { rm "${err}" run "docker rm -f ${docker_client_name}" || true run "docker rm -f ${docker_server_name}" || true - exit 1 + return 1 fi printf "[TEST] [OK] No warnings detected.\\r\\n" @@ -163,7 +182,7 @@ run_remote_backup() { rm "${err}" run "docker rm -f ${docker_client_name}" || true run "docker rm -f ${docker_server_name}" || true - exit 1 + return 1 fi printf "[TEST] [OK] Backup type: '%s' backup.\\r\\n" "${backup_type}" @@ -176,7 +195,7 @@ run_remote_backup() { rm "${err}" run "docker rm -f ${docker_client_name}" || true run "docker rm -f ${docker_server_name}" || true - exit 1 + return 1 fi printf "[TEST] [OK] Latest symlink available: %s\\r\\n" "${ssh_string}:${pwd}${dst_dir}/current" @@ -189,7 +208,7 @@ run_remote_backup() { rm "${err}" run "docker rm -f ${docker_client_name}" || true run "docker rm -f ${docker_server_name}" || true - exit 1 + return 1 fi printf "[TEST] [OK] No '.inprogress' directory found\\r\\n" diff --git a/tests/06-run-local-crazy-filename-chars.sh b/tests/06-run-local-crazy-filename-chars.sh new file mode 100755 index 0000000..e2e49f9 --- /dev/null +++ b/tests/06-run-local-crazy-filename-chars.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +FUNCPATH="${SCRIPTPATH}/.lib/functions.sh" +# shellcheck disable=SC1090 +. "${FUNCPATH}" + + +### +### RSYNC ARGUMENTS +### +RSYNC_ARGS="-- --copy-links" + +print_section "06 crazy filename chars" + +### ################################################################################################ +### ################################################################################################ +### +### CREATE FILES AND DIRS +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Creating files and directories" + +### +### Create source and target dir +### +SRC_DIR="$( create_tmp_dir )" +DST_DIR="$( create_tmp_dir )" + +FILE1_NAME="file 1.txt" +FILE2_NAME="file'2.txt" +FILE3_NAME="file\"3.txt" +FILE4_NAME="file\\4.txt" +FILE5_NAME="sub sub/file5\\\".txt" +FILE6_NAME="sub 'sub/file6\\'.txt" +FILE7_NAME="sub \"sub/file7.txt" +FILE8_NAME="sub \\sub/file*" + +FILE1_PERM="607" +FILE2_PERM="707" +FILE3_PERM="607" +FILE4_PERM="607" +FILE5_PERM="607" +FILE6_PERM="607" +FILE7_PERM="607" +FILE8_PERM="607" + +### +### Create source files +### +create_file "${SRC_DIR}" "${FILE1_NAME}" "2" "${FILE1_PERM}" +create_file "${SRC_DIR}" "${FILE2_NAME}" "5" "${FILE2_PERM}" +create_file "${SRC_DIR}" "${FILE3_NAME}" "1" "${FILE3_PERM}" +create_file "${SRC_DIR}" "${FILE4_NAME}" "1" "${FILE4_PERM}" +create_file "${SRC_DIR}" "${FILE5_NAME}" "1" "${FILE5_PERM}" +create_file "${SRC_DIR}" "${FILE6_NAME}" "1" "${FILE6_PERM}" +create_file "${SRC_DIR}" "${FILE7_NAME}" "1" "${FILE7_PERM}" +create_file "${SRC_DIR}" "${FILE8_NAME}" "1" "${FILE8_PERM}" + + + +### ################################################################################################ +### ################################################################################################ +### +### DEFINE CHECKS +### +### ################################################################################################ +### ################################################################################################ + +check_file() { + local file="${1}" + local destination= + destination="${DST_DIR}/current/$(basename "${SRC_DIR}")" + + print_subline "Validate ${file}" + check_dst_file_is_file "${file}" "${destination}" + check_src_dst_file_exist "${file}" "${SRC_DIR}" "${destination}" + check_src_dst_file_equal "${file}" "${SRC_DIR}" "${destination}" +} + +check_link() { + local link="${1}" + local destination= + destination="${DST_DIR}/current/$(basename "${SRC_DIR}")" + + print_subline "Validate ${link}" + check_dst_file_is_file "${link}" "${destination}" + check_src_dst_file_exist "${file}" "${SRC_DIR}" "${destination}" + check_src_dst_file_equal "${link}" "${SRC_DIR}" "${destination}" +} + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 1) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 1)" + +print_subline "Run Backup" +run_backup \ + "${SCRIPTPATH}/../timemachine" \ + "${SRC_DIR}" \ + "${DST_DIR}" \ + "${RSYNC_ARGS}" \ + "full" + +check_file "${FILE1_NAME}" +check_file "${FILE2_NAME}" +check_file "${FILE3_NAME}" +check_file "${FILE4_NAME}" +check_file "${FILE5_NAME}" +check_file "${FILE6_NAME}" +check_file "${FILE7_NAME}" +check_file "${FILE8_NAME}" + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 2) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 2)" + +print_subline "Run Backup" +run_backup \ + "${SCRIPTPATH}/../timemachine" \ + "${SRC_DIR}" \ + "${DST_DIR}" \ + "${RSYNC_ARGS}" \ + "incremental" + +check_file "${FILE1_NAME}" +check_file "${FILE2_NAME}" +check_file "${FILE3_NAME}" +check_file "${FILE4_NAME}" +check_file "${FILE5_NAME}" +check_file "${FILE6_NAME}" +check_file "${FILE7_NAME}" +check_file "${FILE8_NAME}" diff --git a/tests/06-run-local-crazy-pathname-chars.sh b/tests/06-run-local-crazy-pathname-chars.sh new file mode 100755 index 0000000..b8f0077 --- /dev/null +++ b/tests/06-run-local-crazy-pathname-chars.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +FUNCPATH="${SCRIPTPATH}/.lib/functions.sh" +# shellcheck disable=SC1090 +. "${FUNCPATH}" + + +### +### RSYNC ARGUMENTS +### +RSYNC_ARGS="-- --copy-links" + +print_section "06 crazy pathname chars" + +### ################################################################################################ +### ################################################################################################ +### +### CREATE FILES AND DIRS +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Creating files and directories" + +### +### Create source and target dir +### +echo "# Create SRC_DIR" +SRC_DIR="$( create_tmp_dir "1" "" " \"\\ \` \\# \$统一码-src'" )" +echo "# Create DST_DIR" +DST_DIR="$( create_tmp_dir "1" "" " '\\\" \` \\# \$统一码-dst" )" + +FILE1_NAME="file 1.txt" +FILE2_NAME="file'2.txt" +FILE3_NAME="file\"3.txt" +FILE4_NAME="file\\4.txt" +FILE5_NAME="sub sub/file5\\\".txt" +FILE6_NAME="sub 'sub/file6\\'.txt" +FILE7_NAME="sub \"sub/file7.txt" +FILE8_NAME="sub \\sub/file*" + +FILE1_PERM="607" +FILE2_PERM="707" +FILE3_PERM="607" +FILE4_PERM="607" +FILE5_PERM="607" +FILE6_PERM="607" +FILE7_PERM="607" +FILE8_PERM="607" + +### +### Create source files +### +printf "# Create FILE1: %s/%s\\n" "${SRC_DIR}" "${FILE1_NAME}" +create_file "${SRC_DIR}" "${FILE1_NAME}" "2" "${FILE1_PERM}" + +printf "# Create FILE2: %s/%s\\n" "${SRC_DIR}" "${FILE2_NAME}" +create_file "${SRC_DIR}" "${FILE2_NAME}" "5" "${FILE2_PERM}" + +printf "# Create FILE3: %s/%s\\n" "${SRC_DIR}" "${FILE3_NAME}" +create_file "${SRC_DIR}" "${FILE3_NAME}" "1" "${FILE3_PERM}" + +printf "# Create FILE4: %s/%s\\n" "${SRC_DIR}" "${FILE4_NAME}" +create_file "${SRC_DIR}" "${FILE4_NAME}" "1" "${FILE4_PERM}" + +printf "# Create FILE5: %s/%s\\n" "${SRC_DIR}" "${FILE5_NAME}" +create_file "${SRC_DIR}" "${FILE5_NAME}" "1" "${FILE5_PERM}" + +printf "# Create FILE6: %s/%s\\n" "${SRC_DIR}" "${FILE6_NAME}" +create_file "${SRC_DIR}" "${FILE6_NAME}" "1" "${FILE6_PERM}" + +printf "# Create FILE7: %s/%s\\n" "${SRC_DIR}" "${FILE7_NAME}" +create_file "${SRC_DIR}" "${FILE7_NAME}" "1" "${FILE7_PERM}" + +printf "# Create FILE8: %s/%s\\n" "${SRC_DIR}" "${FILE8_NAME}" +create_file "${SRC_DIR}" "${FILE8_NAME}" "1" "${FILE8_PERM}" + + + +### ################################################################################################ +### ################################################################################################ +### +### DEFINE CHECKS +### +### ################################################################################################ +### ################################################################################################ + +check_file() { + local file="${1}" + local destination= + #destination="${DST_DIR}/current/$(basename "${SRC_DIR}")" + destination="${DST_DIR}/current/$(printf "%q" "$(eval "basename ${SRC_DIR}")")" + + print_subline "Validate ${file}" + check_dst_file_is_file "${file}" "${destination}" + check_src_dst_file_exist "${file}" "${SRC_DIR}" "${destination}" + check_src_dst_file_equal "${file}" "${SRC_DIR}" "${destination}" +} + +check_link() { + local link="${1}" + local destination= + destination="${DST_DIR}/current/$(basename "${SRC_DIR}")" + + print_subline "Validate ${link}" + check_dst_file_is_file "${link}" "${destination}" + check_src_dst_file_exist "${file}" "${SRC_DIR}" "${destination}" + check_src_dst_file_equal "${link}" "${SRC_DIR}" "${destination}" +} + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 1) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 1)" + +print_subline "Run Backup" +run_backup \ + "${SCRIPTPATH}/../timemachine" \ + "${SRC_DIR}" \ + "${DST_DIR}" \ + "${RSYNC_ARGS}" \ + "full" + +check_file "${FILE1_NAME}" +check_file "${FILE2_NAME}" +check_file "${FILE3_NAME}" +check_file "${FILE4_NAME}" +check_file "${FILE5_NAME}" +check_file "${FILE6_NAME}" +check_file "${FILE7_NAME}" +check_file "${FILE8_NAME}" + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 2) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 2)" + +print_subline "Run Backup" +run_backup \ + "${SCRIPTPATH}/../timemachine" \ + "${SRC_DIR}" \ + "${DST_DIR}" \ + "${RSYNC_ARGS}" \ + "incremental" + +check_file "${FILE1_NAME}" +check_file "${FILE2_NAME}" +check_file "${FILE3_NAME}" +check_file "${FILE4_NAME}" +check_file "${FILE5_NAME}" +check_file "${FILE6_NAME}" +check_file "${FILE7_NAME}" +check_file "${FILE8_NAME}" diff --git a/tests/13-run-remote-crazy-source-pathname-chars.sh b/tests/13-run-remote-crazy-source-pathname-chars.sh new file mode 100755 index 0000000..36ba893 --- /dev/null +++ b/tests/13-run-remote-crazy-source-pathname-chars.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +FUNCPATH="${SCRIPTPATH}/.lib/functions.sh" +# shellcheck disable=SC1090 +. "${FUNCPATH}" + + +### +### GLOBALS +### +#SSH_USER="root" +SSH_HOST="server" +#SSH_PORT="22" + +TIMEMACHINE_ARGS="" +RSYNC_ARGS="--progress" + +print_section "12 Remote (crazy pathname chars)" + +### ################################################################################################ +### ################################################################################################ +### +### CREATE FILES AND DIRS +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Creating files and directories" + +### +### Create source dir +### +SRC_DIR="$( create_tmp_dir )" +DOCKER_SRC_DIR="/$(printf "%s" ' "\ ` $统一码-$-src')" +DOCKER_DST_DIR="/backup-dst" + +FILE1_NAME="file1.txt" +FILE2_NAME="file2.txt" +FILE3_NAME="sub/file3.txt" + +FILE1_PERM="607" +FILE2_PERM="707" +FILE3_PERM="607" + +### +### Create source files +### +create_file "${SRC_DIR}" "${FILE1_NAME}" "2" "${FILE1_PERM}" +create_file "${SRC_DIR}" "${FILE2_NAME}" "5" "${FILE2_PERM}" +create_file "${SRC_DIR}" "${FILE3_NAME}" "1" "${FILE3_PERM}" + + +### ################################################################################################ +### ################################################################################################ +### +### DEFINE CHECKS +### +### ################################################################################################ +### ################################################################################################ + +check() { + printf "" +} + + +### ################################################################################################ +### ################################################################################################ +### +### Start container +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Start container" + +### +### Kill accidentally left artifacts +### +run "docker rm -f ssh-server || true" >/dev/null 2>&1 +run "docker rm -f ssh-client || true" >/dev/null 2>&1 + +### +### Startup +### +run "docker run -d --rm --name ssh-server -h server cytopia/ssh-server /usr/sbin/sshd -D" +run "docker exec ssh-server mkdir $(printf "%q" "${DOCKER_DST_DIR}")" +run "docker run -d --rm --name ssh-client -h client --link ssh-server -v '${SCRIPTPATH}/../timemachine:/usr/bin/timemachine' -v ${SRC_DIR}:/$(printf "%q" "${DOCKER_SRC_DIR}") cytopia/ssh-client" +run "sleep 5" + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 1) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 1)" + +print_subline "Run Backup" +run_remote_backup \ + "ssh-client" \ + "ssh-server" \ + "/usr/bin/timemachine" \ + "${TIMEMACHINE_ARGS}" \ + "${DOCKER_SRC_DIR}" \ + "${SSH_HOST}" \ + "${DOCKER_DST_DIR}" \ + "${RSYNC_ARGS}" \ + "full" + +check "${FILE1_NAME}" "${FILE1_PERM}" +check "${FILE2_NAME}" "${FILE2_PERM}" +check "${FILE3_NAME}" "${FILE3_PERM}" + + +### ################################################################################################ +### ################################################################################################ +### +### Run backup (Round 2) +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Backup (Round 2)" + +print_subline "Run Backup" +run_remote_backup \ + "ssh-client" \ + "ssh-server" \ + "/usr/bin/timemachine" \ + "${TIMEMACHINE_ARGS}" \ + "${DOCKER_SRC_DIR}" \ + "${SSH_HOST}" \ + "${DOCKER_DST_DIR}" \ + "${RSYNC_ARGS}" \ + "incremental" + +check "${FILE1_NAME}" "${FILE1_PERM}" +check "${FILE2_NAME}" "${FILE2_PERM}" +check "${FILE3_NAME}" "${FILE3_PERM}" + + +### ################################################################################################ +### ################################################################################################ +### +### Cleanup +### +### ################################################################################################ +### ################################################################################################ + +print_headline "Cleanup" + +### +### Remove artifacts +### +run "docker rm -f ssh-server" +run "docker rm -f ssh-client" diff --git a/timemachine b/timemachine index 075cc75..b6cc558 100755 --- a/timemachine +++ b/timemachine @@ -13,8 +13,8 @@ MY_DESC="OSX-like timemachine cli script for Linux and BSD (and even OSX)" MY_PROJ="https://github.com/cytopia/linux-timemachine" MY_AUTH="cytopia" MY_MAIL="cytopia@everythingcli.org" -MY_VERS="1.3" -MY_DATE="2021-04-08" +MY_VERS="1.3.1" +MY_DATE="2021-12-02" # Default command line arguments VERBOSE= @@ -27,7 +27,7 @@ SSH_ARGS="-oStrictHostKeyChecking=no -oLogLevel=QUIET -q" ################################################################################ -# Functions +# Info Functions ################################################################################ print_usage() { @@ -101,6 +101,12 @@ print_version() { echo "${MY_PROJ}" } + + +################################################################################ +# Logging Functions +################################################################################ + logdebug() { # Only log to stdout when verbose is turned on if [ "${VERBOSE}" = "debug" ]; then @@ -119,11 +125,36 @@ logerr() { echo "$(date +'%Y-%m-%d %H:%M:%S') ${MY_NAME}: [ERROR] ${*}" >&2 } + + +################################################################################ +# Backup Functions +################################################################################ + +### +### POSIX compliant path escape (like printf "%q" in bash) +### +escape_path() { + # POSIX version + # https://mullikine.github.io/posts/missing-posix-shell-functions-cmd-and-myeval/ + for var in "${@}"; do + #printf "%s" "$(printf %s "${var}" | sed "s/'/'\\\\''/g")"; + printf "'%s' " "$(printf %s "${var}" | sed "s/'/'\\\\''/g")"; + done | sed 's/ $//' +} + ### ### Check if the destination is a remote server ### is_remote() { - echo "${1}" | grep -E '.+:.+' >/dev/null + logdebug "Checking if dir is remote (SSH) or local" + if echo "${1}" | grep -E '.+:.+' >/dev/null; then + logdebug "Dir is remote: ${1}" + return 0 + else + logdebug "Dir is local: ${1}" + return 1 + fi } ### @@ -135,12 +166,25 @@ dir_exists() { if is_remote "${directory}"; then ssh_part="$( echo "${directory}" | awk -F':' '{print $1}' )" dir_part="$( echo "${directory}" | awk -F':' '{print $2}' )" - cmd="ssh ${SSH_ARGS} ${ssh_part} 'test -d \"${dir_part}\"'" + dir_part="$( escape_path "${dir_part}" )" + ssh_cmd="test -d ${dir_part}" + cmd="ssh ${SSH_ARGS} ${ssh_part} ${ssh_cmd}" + logdebug "Checking if remote dir exists: ${dir_part}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}"; then + logerr "Remote directory does not exist: ${dir_part}" + return 1 + fi else - cmd="test -d '${directory}'" + directory="$( escape_path "${directory}" )" + cmd="test -d ${directory}" + logdebug "Checking if local dir exists: ${directory}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logerr "Local directory does not exist: ${dir_part}" + return 1 + fi fi - logdebug "${cmd}" - eval "${cmd}" >/dev/null } ### @@ -152,29 +196,25 @@ link_exists() { if is_remote "${directory}"; then ssh_part="$( echo "${directory}" | awk -F':' '{print $1}' )" dir_part="$( echo "${directory}" | awk -F':' '{print $2}' )" - cmd="ssh ${SSH_ARGS} ${ssh_part} 'test -L \"${dir_part}\"'" + dir_part="$( escape_path "${dir_part}" )" + ssh_cmd="test -L ${dir_part}" + cmd="ssh ${SSH_ARGS} ${ssh_part} ${ssh_cmd}" + logdebug "Checking if remote symlink exists: ${dir_part}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logdebug "Remote symlink does not exist: ${dir_part}" + return 1 + fi else - cmd="test -L '${directory}'" + directory="$( escape_path "${directory}" )" + cmd="test -L ${directory}" + logdebug "Checking if local symlink exists: ${directory}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logdebug "Local symlink does not exist: ${directory}" + return 1 + fi fi - logdebug "${cmd}" - eval "${cmd}" >/dev/null -} - -### -### Remove local or remote file -### -remove_file() { - file="${1}" - - if is_remote "${file}"; then - ssh_part="$( echo "${file}" | awk -F':' '{print $1}' )" - file_part="$( echo "${file}" | awk -F':' '{print $2}' )" - cmd="ssh ${SSH_ARGS} ${ssh_part} 'rm \"${file_part}\"'" - else - cmd="rm '${file}'" - fi - logdebug "${cmd}" - eval "${cmd}" >/dev/null } ### @@ -188,12 +228,27 @@ rename_directory() { ssh_part="$( echo "${from}" | awk -F':' '{print $1}' )" dir_from_part="$( echo "${from}" | awk -F':' '{print $2}' )" dir_to_part="$( echo "${to}" | awk -F':' '{print $2}' )" - cmd="ssh ${SSH_ARGS} ${ssh_part} 'mv \"${dir_from_part}\" \"${dir_to_part}\"'" + dir_from_part="$( escape_path "${dir_from_part}" )" + dir_to_part="$( escape_path "${dir_to_part}" )" + ssh_cmd="mv ${dir_from_part} ${dir_to_part}" + cmd="ssh ${SSH_ARGS} ${ssh_part} ${ssh_cmd}" + logdebug "Renaming remote dir: ${dir_from_part} -> ${dir_to_part}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logerr "Failed to rename remote dir" + return 1 + fi else - cmd="mv '${from}' '${to}'" + from="$( escape_path "${from}" )" + to="$( escape_path "${to}" )" + cmd="mv ${from} ${to}" + logdebug "Renaming local dir: ${from} -> ${to}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logerr "Failed to rename local dir" + return 1 + fi fi - logdebug "${cmd}" - eval "${cmd}" >/dev/null } ### @@ -206,12 +261,27 @@ link_directory() { if is_remote "${lnk}"; then ssh_part="$( echo "${lnk}" | awk -F':' '{print $1}' )" lnk_part="$( echo "${lnk}" | awk -F':' '{print $2}' )" - cmd="ssh ${SSH_ARGS} ${ssh_part} 'ln -s \"${dir}\" \"${lnk_part}\"'" + dir="$( escape_path "${dir}" )" + lnk_part="$( escape_path "${lnk_part}" )" + ssh_cmd="ln -sfn ${dir} ${lnk_part}" + cmd="ssh ${SSH_ARGS} ${ssh_part} ${ssh_cmd}" + logdebug "Creating remote symlink: ${lnk}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logerr "Failed to created remote symlink: ${lnk}" + return 1 + fi else - cmd="ln -s '${dir}' '${lnk}'" + dir="$( escape_path "${dir}" )" + lnk="$( escape_path "${lnk}" )" + cmd="ln -sfn ${dir} ${lnk}" + logdebug "Creating local symlink: ${lnk}" + logdebug "\$ ${cmd}" + if ! eval "${cmd}" >/dev/null; then + logerr "Failed to created local symlink: ${lnk}" + return 1 + fi fi - logdebug "${cmd}" - eval "${cmd}" >/dev/null } @@ -221,50 +291,61 @@ link_directory() { ################################################################################ # Parse input args with getopts -while getopts :vdpi:hV-: opt; do - # ----- long options - if [ "${opt}" = "-" ]; then - opt=${OPTARG} - [ -z "${opt}" ] && break # "--" terminates argument processing - fi - # shellcheck disable=SC2214 - case "${opt}" in - # ---- Help / version - V | version) +while test "${#}" -gt 0; do + case "${1}" in + # ---------- Help / version ---------- + -V | --version) print_version exit ;; - h | help) + -h | --help) print_usage exit ;; - # ----- Options - p | port) + # ---------- Verbosity ---------- + -v | --verbose) + if [ "${VERBOSE}" != "debug" ]; then + VERBOSE="verbose" + fi + shift + ;; + -d | --debug) + VERBOSE="debug" + shift + ;; + # ---------- Options ---------- + -p | --port) + if [ -z "${2:-}" ]; then + logerr "${1} requires an argument, see -h for help." + exit 2 + fi shift PORT="${1}" SSH_ARGS="${SSH_ARGS} -p ${PORT}" + shift ;; - i | identity) + -i | --identity) + if [ -z "${2:-}" ]; then + logerr "${1} requires an argument, see -h for help." + exit 2 + fi shift KEY="${1}" SSH_ARGS="${SSH_ARGS} -i ${KEY}" + shift ;; - v | verbose) - if [ "${VERBOSE}" != "debug" ]; then - VERBOSE="verbose" + -*) + if [ "${1}" != "--" ]; then + logerr "Unknown option ${1}, see -h for help." + exit 2 fi + shift ;; - d | debug) - VERBOSE="debug" - ;; - \?) logerr "Unknown option -${OPTARG}, see -h for help." - exit 2 - ;; - *) logerr "Unknown option --${opt}, see -h for help." - exit 2 - ;; + *) + # No more options available, so break and evaluate + # positional arguments. + break esac - shift done @@ -274,23 +355,27 @@ done ################################################################################ if [ "${#}" -lt "2" ]; then - logerr " and are required. See -h for help." + logerr " and are required." + logerr "See -h for help." exit 1 fi if is_remote "${1}"; then if is_remote "${2}"; then logerr "Source and Target cannot both be remote locations." + logerr "See -h for help." exit 1 fi fi if ! dir_exists "${1}"; then - logerr "Source directory does not exist: '${1}'. See -h for help." + logerr "Source directory does not exist: ${1}" + logerr "See -h for help." exit 1 fi if ! dir_exists "${2}"; then - logerr "Target directory does not exist: '${2}'. See -h for help." + logerr "Target directory does not exist: ${2}" + logerr "See -h for help." exit 1 fi @@ -340,11 +425,12 @@ BTYPE= # Only link destination if it already exists if link_exists "${DEST}/${BACKUP_LATEST}"; then BTYPE="incremental" + logmsg "Starting incremental backup" - logmsg "\$ rsync $* ${SRC} ${DEST}/${BACKUP_INPROGRESS}" + logmsg "\$ rsync $* $( escape_path "${SRC}" ) $( escape_path "${DEST}/${BACKUP_INPROGRESS}" )" - rsync \ - -e "ssh ${SSH_ARGS}" \ + cmd="rsync \ + -e \"ssh ${SSH_ARGS}\" \ --recursive \ --perms \ --owner \ @@ -353,17 +439,18 @@ if link_exists "${DEST}/${BACKUP_LATEST}"; then --links \ --delete \ --delete-excluded \ - --partial-dir="${RSYNC_PARTIAL}" \ - --link-dest="../${BACKUP_LATEST}" \ - "$@" \ - "${SRC}" "${DEST}/${BACKUP_INPROGRESS}" + --partial-dir=${RSYNC_PARTIAL} \ + --link-dest=../${BACKUP_LATEST} \ + $* \ + $( escape_path "${SRC}" ) $( escape_path "${DEST}/${BACKUP_INPROGRESS}" )" else BTYPE="full" + logmsg "Starting full backup" - logmsg "\$ rsync $* ${SRC} ${DEST}/${BACKUP_INPROGRESS}" + logmsg "\$ rsync $* $( escape_path "${SRC}" ) $( escape_path "${DEST}/${BACKUP_INPROGRESS}" )" - rsync \ - -e "ssh ${SSH_ARGS}" \ + cmd="rsync \ + -e \"ssh ${SSH_ARGS}\" \ --recursive \ --perms \ --owner \ @@ -372,34 +459,29 @@ else --links \ --delete \ --delete-excluded \ - --partial-dir="${RSYNC_PARTIAL}" \ - "$@" \ - "${SRC}" "${DEST}/${BACKUP_INPROGRESS}" + --partial-dir=${RSYNC_PARTIAL} \ + $* \ + $( escape_path "${SRC}" ) $( escape_path "${DEST}/${BACKUP_INPROGRESS}" )" +fi + +if ! eval "${cmd}"; then + logerr "${MY_NAME} Backup has failed" + exit 1 fi ### ### 2/3 Finish atomic operation ### - # Move temporary atomic directory to chosen dest directory -logmsg "\$ mv ${DEST}/${BACKUP_INPROGRESS} ${DEST}/${BACKUP}" rename_directory "${DEST}/${BACKUP_INPROGRESS}" "${DEST}/${BACKUP}" ### ### 3/3 Latest symlink ### - -# Remove current 'latest' symlink -if link_exists "${DEST}/${BACKUP_LATEST}"; then - logmsg "\$ rm ${DEST}/${BACKUP_LATEST}" - remove_file "${DEST}/${BACKUP_LATEST}" -fi - -# Set new 'latest' link-dest for incremental backups -logmsg "\$ ln -s ${BACKUP} ${DEST}/${BACKUP_LATEST}" link_directory "${BACKUP}" "${DEST}/${BACKUP_LATEST}" +link_exists "${DEST}/${BACKUP_LATEST}" ###