Bash version of trsync
diff --git a/functions/common.sh b/functions/common.sh
new file mode 100644
index 0000000..9d0d956
--- /dev/null
+++ b/functions/common.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+cleanup_and_exit()
+{
+    trap EXIT
+    exit ${1:-0}
+}
+
+fail_exit()
+{
+    echo "$@"
+    cleanup_and_exit 1
+}
+
+job_lock() {
+    [ -z "$1" ] && fail_exit "Lock file is not specified"
+    local LOCKFILE=$1
+    shift
+    local fd=1000
+    eval "exec $fd>>$LOCKFILE"
+    case $1 in
+        "set")
+            flock -x -n $fd \
+                || fail_exit "Process already running. Lockfile: $LOCKFILE"
+            ;;
+        "unset")
+            flock -u $fd
+            rm -f $LOCKFILE
+            ;;
+        "wait")
+            local TIMEOUT=${2:-3600}
+            [ "${VERBOSE}" == "true" ] \
+                && echo "Waiting of concurrent process (lockfile: $LOCKFILE, timeout = $TIMEOUT seconds) ..."
+            flock -x -w $TIMEOUT $fd \
+                || fail_exit "Timeout error (lockfile: $LOCKFILE)"
+            ;;
+    esac
+}
+
+trap fail_exit EXIT
diff --git a/functions/rsync.sh b/functions/rsync.sh
new file mode 100644
index 0000000..eabdf20
--- /dev/null
+++ b/functions/rsync.sh
@@ -0,0 +1,217 @@
+#!/bin/bash -xe
+
+export LANG=C
+
+# define this vars before use
+FILESDIR=${FILESDIR:-"snapshots"}
+LATESTSUFFIX=${LATESTSUFFIX:-"-latest"}
+
+export TIMESTAMP=${TIMESTAMP:-$(date "+%Y-%m-%d-%H%M%S")}
+export SAVE_LAST_DAYS=${SAVE_LAST_DAYS:-61}
+export WARN_DATE=$(date "+%Y%m%d" -d "$SAVE_LAST_DAYS days ago")
+
+get_empty_dir() {
+    mktemp -d
+}
+
+get_symlink() {
+    local LINKDEST=$1
+    local LINKNAME=$(mktemp -u)
+    ln -s --force "$LINKDEST" "$LINKNAME" && echo "$LINKNAME"
+}
+
+rsync_list() {
+    local RSYNCPATH=$1
+    local TEMPFILE=$(mktemp)
+    local RESULT=1
+    if rsync -l "${RSYNCPATH}/" 2>/dev/null > "$TEMPFILE" ; then
+        grep -v '\.$' "$TEMPFILE" || :
+        RESULT=0
+    fi
+    rm "$TEMPFILE"
+    return "$RESULT"
+}
+
+rsync_list_links() {
+    local RSYNCPATH=$1
+    local TEMPFILE=$(mktemp)
+    local RESULT=1
+    if rsync_list "$RSYNCPATH" > "$TEMPFILE" ; then
+        grep '^l' "$TEMPFILE" | awk '{print $(NF-2)" "$NF}' || :
+        RESULT=0
+    fi
+    rm "$TEMPFILE"
+    return "$RESULT"
+}
+
+rsync_list_dirs() {
+    local RSYNCPATH=$1
+    local TEMPFILE=$(mktemp)
+    local RESULT=1
+    if rsync_list "$RSYNCPATH" > "$TEMPFILE" ; then
+        grep '^d' "$TEMPFILE" | awk '{print $NF}' || :
+        RESULT=0
+    fi
+    rm "$TEMPFILE"
+    return "$RESULT"
+}
+
+rsync_list_files() {
+    local RSYNCPATH=$1
+    local TEMPFILE=$(mktemp)
+    local RESULT=1
+    if rsync_list "$RSYNCPATH" > "$TEMPFILE" ; then
+        grep -vE '^d|^l' "$TEMPFILE" | awk '{print $NF}' || :
+        RESULT=0
+    fi
+    rm "$TEMPFILE"
+    return "$RESULT"
+}
+
+rsync_delete_file() {
+    local RSYNCPATH=${1%/}
+    local FILENAME=${RSYNCPATH##*/}
+    local EMPTYDIR=$(get_empty_dir)
+    if rsync_list_files "$RSYNCPATH" &>/dev/null ; then
+        [ -n "$IS_VERBOSE" ] && echo "[info] Deleting file ${RSYNCPATH}"
+        rsync -rv --delete --include="$FILENAME" '--exclude=*' \
+            "${EMPTYDIR}/" "${RSYNCPATH%/*}/"
+        [ -d "$EMPTYDIR" ] && rm -rf "$EMPTYDIR"
+    fi
+}
+
+rsync_delete_dir() {
+    local RSYNCPATH=$1
+    if rsync_list "${RSYNCPATH}" &>/dev/null ; then
+        local EMPTYDIR=$(get_empty_dir)
+        [ -n "$IS_VERBOSE" ] && echo "[info] Deleting directory ${RSYNCPATH}/"
+        rsync --delete -a "${EMPTYDIR}/" "${RSYNCPATH}/" \
+            && rsync_delete_file "$RSYNCPATH"
+        [ -d "$EMPTYDIR" ] && rm -rf "$EMPTYDIR"
+    fi
+}
+
+rsync_create_dir() {
+    local RSYNCPATH=$1
+    if ! rsync_list_dirs "${RSYNCPATH%/*}" &>/dev/null ; then
+        rsync_create_dir "${RSYNCPATH%/*}"
+    fi
+    if ! rsync_list_dirs "${RSYNCPATH}" &>/dev/null ; then
+        [ -n "$IS_VERBOSE" ] && echo "[info] Creating directory ${RSYNCPATH}/"
+        local EMPTYDIR=$(get_empty_dir)
+        rsync -a "${EMPTYDIR}/" "${RSYNCPATH}/"
+        [ -d "$EMPTYDIR" ] && rm -rf "$EMPTYDIR"
+    fi
+}
+
+rsync_create_symlink() {
+    # Create symlink $1 -> $2
+    # E.g. "create_symlink host repos/6.1 files/6.1-stable"
+    # wll create symlink repos/6.1 -> repos/files/6.1-stable
+    local RSYNCPATH=$1
+    local LINKDEST=$2
+    [ -n "$IS_VERBOSE" ] && echo "[info] Creating symlink $RSYNCPATH to $LINKDEST"
+    local SYMLINK_FILE=$(get_symlink "$LINKDEST")
+    rsync -vl "$SYMLINK_FILE" "${RSYNCPATH}"
+    rm "$SYMLINK_FILE"
+
+#    # Make text file for dereference symlinks
+#    local TARGET_TXT_FILE=$(mktemp)
+#    echo "$LINKDEST" > "$TARGET_TXT_FILE"
+#    rsync -vl "$TARGET_TXT_FILE" "${RSYNCHOST}/${LINKNAME}.target.txt"
+#    rm "$TARGET_TXT_FILE"
+}
+
+#######################################################
+rsync_remove_old_snapshots() {
+    # Remove mirrors older then $SAVE_LAST_DAYS and w/o symlinks on it
+    local RSYNCPATH=${1%%+(/)}
+
+    local FOLDERNAME=${RSYNCPATH##*/}
+    local DIRS=$(rsync_list_dirs "${RSYNCPATH%/*}/$FILESDIR" | grep "^$FOLDERNAME\-" )
+    local dir
+    for dir in $DIRS; do
+        local ddate=$(echo "$dir" | awk -F '[-]' '{print $(NF-3)$(NF-2)$(NF-1)}')
+        [ "$ddate" -gt "$WARN_DATE" ] && continue
+        LINKS=$(rsync_list_links "${RSYNCPATH%/*}/$FILESDIR" | grep " $dir$" \
+            | awk '{print $1}'; \
+            rsync_list_links "${RSYNCPATH%/*}" | grep " ${FILESDIR}/$dir$" \
+            | awk '{print $1}')
+        if [ "$LINKS" = "" ]; then
+            [ -n "$IS_VERBOSE" ] \
+                && echo "[info] Delete snapshot $dir as obsoleted"
+            rsync_delete_dir "${RSYNCPATH%/*}/${FILESDIR}/$dir"
+            continue
+        fi
+        [ -n "$IS_VERBOSE" ] \
+            && echo "[info] $dir Can't be deleted because of synlinks: $LINKS"
+    done
+}
+
+######################################################
+rsync_transfer() {
+    # sync files to remote host
+    # $1 - remote path
+    # $2 - source dir
+    # $3 - dest dir name (the same as source dir name if empty)
+    # $4 - extra params for rsync (optional)
+    local SRCDIR=${1%%+(/)}
+    local RSYNCPATH=${2%%+(/)}
+    local FOLDERNAME=${3%%+(/)}
+    local RSYNC_EXTRA_PARAMS=$4
+    [ -z "$FOLDERNAME" ] && FOLDERNAME=${SRCDIR##*/}
+
+    # Lock source dir to prevent concurent sync stage
+    job_lock ${SRCDIR}.lock wait
+    rsync_list_dirs "${RSYNCPATH}/$FILESDIR" &>/dev/null \
+        || rsync_create_dir "${RSYNCPATH}/$FILESDIR"
+
+    # Skip host part of RSYNCPATH
+    local RSYNCROOT
+    if [ "${RSYNCPATH/:\/\/}" != "$RSYNCPATH" ] ; then 
+        # `rsync://[user@]host[:port]/module/*` case
+        RSYNCROOT=$(echo "$RSYNCPATH" | awk -F'/' \
+            '{ for(i=5;i<=NF;++i) printf("/" $i) }')
+    else
+        local _first_part=${RSYNCPATH%%/*}
+        if [ "${_first_part/::}" != "$_first_part" ] ; then
+            # `[user@]host::module/*` case
+            RSYNCROOT=$(echo "$RSYNCPATH" | awk -F'::' '{ print $2 }')
+            RSYNCROOT="/${RSYNCROOT#*/}"
+        elif [ "${_first_part/:}" != "$_first_part" ] ; then
+            # `[user@]host:*` case
+            RSYNCROOT=$(echo "$RSYNCPATH" | awk -F':' '{ print $2 }')
+        else
+            # local path case
+            RSYNCROOT=$RSYNCPATH
+        fi
+    fi
+
+    # Remove leading slashes
+    RSYNCROOT=${RSYNCROOT##+(/)}
+
+    local OPTIONS="--archive --verbose --force --ignore-errors --delete-excluded --no-owner --no-group \
+          $RSYNC_EXTRA_PARAMS --delete --link-dest=/${RSYNCROOT}/${FILESDIR}/${FOLDERNAME}$LATESTSUFFIX"
+
+    local SNAPSHOT_NAME=${FOLDERNAME}-$TIMESTAMP
+    local DEST_DIR=${RSYNCPATH}/${FILESDIR}/$SNAPSHOT_NAME
+    local LATEST_LINK=${FILESDIR}/${FOLDERNAME}$LATESTSUFFIX
+
+    local RESULT=1
+    # shellcheck disable=SC2086
+    if rsync $OPTIONS "${SRCDIR}/" "$DEST_DIR" ; then
+        rsync_delete_file "${RSYNCPATH}/$LATEST_LINK" \
+        && rsync_create_symlink "${RSYNCPATH}/$LATEST_LINK" "$SNAPSHOT_NAME" \
+        && rsync_delete_file "${RSYNCPATH}/$FOLDERNAME" \
+        && rsync_create_symlink "${RSYNCPATH}/$FOLDERNAME" "${FILESDIR}/$SNAPSHOT_NAME" \
+        && rsync_remove_old_snapshots "${RSYNCPATH}/$FOLDERNAME"
+        RESULT=0
+    else
+        rsync_delete_dir "${RSYNCPATH}/$DEST_DIR"
+    fi
+
+    # Unlock source dir
+    job_lock ${SRCDIR}.lock unset
+
+    return $RESULT
+}
diff --git a/trsync.sh b/trsync.sh
new file mode 100755
index 0000000..8f4aa35
--- /dev/null
+++ b/trsync.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -o errexit
+
+BIN_DIR=$(dirname "$(readlink -e "$0")")
+source "${BIN_DIR}/functions/common.sh"
+source "${BIN_DIR}/functions/rsync.sh"
+
+rsync_transfer "$1" "$2"