Skip to content

Instantly share code, notes, and snippets.

@BMTLab
Last active April 25, 2024 07:34
Show Gist options
  • Save BMTLab/8ebb2cbfd5a3ed5c0584bb30831ff5d8 to your computer and use it in GitHub Desktop.
Save BMTLab/8ebb2cbfd5a3ed5c0584bb30831ff5d8 to your computer and use it in GitHub Desktop.
Wrapper for rsync, optimized for faster network transfers. It simplifies rsync's configuration to enhance usability & performance
#!/bin/bash
# shellcheck disable=SC2034
# Author: Nikita Neverov (BMTLab)
# Version: 2.1.7
readonly ERR_FRSYNC_INVALID_OPTIONS=101
#######################################
# frsync: Optimizes the file synchronization process from local to remote using rsync over SSH or vice versa.
# The function is designed for advanced users needing fine-grained control over file transfer settings,
# aiming to achieve the highest possible performance. It includes options for enabling SSH compression,
# utilizing custom SSH private keys, managing SSH connection multiplexing, and ensuring file consistency
# with the --delete option. It also provides mechanisms to bypass SSH security checks in trusted networks
# for faster setup. This function embodies a balance between speed, security, and flexibility, offering
# users a powerful tool for their file synchronization needs.
#
# Globals:
# ERR_FRSYNC_INVALID_OPTIONS - Error code for invalid options.
#
# Arguments:
# -h - Displays help message and exits.
# -c - Enables compression during the transfer to improve speed over slow connections.
# -i <private_key> - Specifies a path to the SSH private key for secure authentication.
# -m - Disables SSH connection multiplexing (ControlMaster and ControlPath), useful in environments where SSH multiplexing might not be supported.
# -d - Enables the --delete option in rsync to ensure the receiving side mirrors the source by deleting extraneous files.
# -u - Enables unsafe SSH options for faster communication in trusted networks, such as disabling strict host key checking.
# <local_path1> [<local_path2> ...] <remote_user@remote_ip:remote_path> - Specifies local and remote paths for rsync.
#
# Outputs:
# Writes messages to stdout and stderr, providing detailed feedback on the transfer process.
# Exits with various status codes based on success or type of error encountered during execution.
#
# Returns:
# 0 - Successful execution.
# ERR_FRSYNC_INVALID_OPTIONS(101) - Returned in case of invalid command-line options.
# Other non-zero status - Returned in case of errors during rsync execution.
#######################################
function frsync() {
local help_message
# Multi-line usage message
read -r -d '' help_message << 'EOF'
Usage: frsync [-h] [-c] [-i private_key] [-m] [-d] [-u] <local_path1> [<local_path2> ...] <remote_user@remote_ip:remote_path>
Options:
-h: Display this help message and exit.
-c: Enable compression during transfer.
-i: Specify a path to the SSH private key for authentication.
-m: Disable SSH connection multiplexing (ControlMaster and ControlPath).
-d: Enable the --delete option in rsync to delete extraneous files from the receiving side.
-u: Enable unsafe SSH options for faster communication, but only in trusted networks.
This function preconfigures rsync for faster network transfers by optimizing various settings.
EOF
# Prints help message
function __frsync_usage() {
printf '%b\n' "$help_message"
unset -f __frsync_usage
}
# Prints error message
function __frsync_error() {
local -r message="$1"
local -ir code="${2:-$ERR_FRSYNC_INVALID_OPTIONS}"
printf 'Error: %s.\n\n' "$message" >&2
__frsync_usage
unset -f __frsync_error
return $code
}
local private_key_option=''
local is_private_key_option_set=false
local enable_compression=false
local disable_multiplexing=false
local enable_rsync_delete=false
local enable_unsafe_options=false
## Process command-line options
# This ensures that getopts always starts processing arguments from the first argument each time
# the frsync function is called, and should fix the problem of missing options on repeated calls.
OPTIND=1
local opt
while getopts ':hci:mdu' opt; do
case "$opt" in
h)
__frsync_usage
return 0
;;
c)
enable_compression=true
;;
i)
if [[ -z $OPTARG ]]; then
__frsync_error 'Option -i requires an argument' || return $?
fi
private_key_option="$OPTARG"
is_private_key_option_set=true
;;
m)
disable_multiplexing=true
;;
d)
enable_rsync_delete=true
;;
u)
enable_unsafe_options=true
;;
\?)
__frsync_error "Invalid option: -${OPTARG}" || return $?
;;
:)
__frsync_error "Option -${OPTARG} requires an argument" || return $?
;;
esac
done
# We remove the processed options, leaving only the path arguments
shift $((OPTIND - 1))
# Verify that at least two arguments remain (local and remote paths)
if [ "$#" -lt 2 ]; then
__frsync_error 'Local and remote host must be specified' || return $?
fi
# Calculates local and remote hosts
local -r remote_path="${!#}" # Get the last argument as the remote path
local -ar local_path_arr=("${@:1:$#-1}") # Define as an array of all arguments except the last one
# Tuning RSYNC options
local -a rsync_options_arr=('-aHAXxv' '--numeric-ids' '--progress' '--partial' '--inplace')
[[ $enable_rsync_delete == true ]] && rsync_options_arr+=('--delete')
[[ $enable_compression == true ]] && rsync_options_arr+=('-z')
# Tuning SSH options
local -a ssh_options_arr=('-T' '-c' 'chacha20-poly1305@openssh.com') # Fastest encryption algorthym
[[ $is_private_key_option_set == true ]] && ssh_options_arr+=('-i' "$private_key_option")
[[ $disable_multiplexing == false ]] && ssh_options_arr+=('-o' 'ControlMaster=auto' '-o' 'ControlPath=/tmp/frsync-%r@%h:%p')
[[ $enable_unsafe_options == true ]] && ssh_options_arr+=('-o' 'StrictHostKeyChecking=no' '-o' 'UserKnownHostsFile=/dev/null')
ssh_options_arr+=('-o' 'ForwardX11=no')
# ssh_options_arr+=('-v') # DEBUG
local -r ssh_command="ssh $(printf '%q ' "${ssh_options_arr[@]}")"
# Execute rsync with the predefined options
rsync "${rsync_options_arr[@]}" -e "$ssh_command" "${local_path_arr[@]}" "$remote_path"
unset -f __frsync_usage __frsync_error
}
readonly -f frsync
@BMTLab
Copy link
Author

BMTLab commented Apr 25, 2024

#!/usr/bin/env bats

load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'

setup() {
  source ./.frsync.sh
}

@test 'Display help message with -h option' {
  ## Act
  run frsync -h

  ## Assert
  assert_success
  assert_output --partial 'Usage: frsync'
}

@test 'Error with invalid option' {
  ## Act
  run frsync -z

  ## Assert
  assert_failure
  assert_output --partial 'Error: Invalid option'
}

@test 'Error when mandatory arguments are not provided' {
  ## Act
  run frsync

  ## Assert
  assert_failure
  assert_output --partial 'Local and remote host must be specified'
}

@test 'Enable compression with -c option' {
  ## Arrange
  # Mocking `rsync` command to prevent actual execution
  # shellcheck disable=SC2317
  function rsync() {
    echo "rsync called with args: $*"
  }
  export -f rsync

  ## Act
  run frsync -c 'source' 'target'

  ## Assert
  assert_success
  assert_output --partial '-z'
}

@test 'Specify private key with -i option' {
  ## Arrange
  # Mocking `rsync` command to prevent actual execution
  # shellcheck disable=SC2317
  function rsync() {
    echo "rsync called with args: $*"
  }
  export -f rsync

  ## Act
  run frsync -i '/path/to/key' 'source' 'target'

  ## Assert
  assert_success
  assert_output --partial '-i /path/to/key'
}

teardown() {
  unset -f rsync
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment