|
#!/usr/bin/env bash |
|
#|----------------------------------------------------------------------------- |
|
#| SYMC: A directory <-> directory syncing utility using symlinks. |
|
#| Copyright (C) 2018 eth-p |
|
#| |
|
#| This tool is designed to symlink files between two directories, |
|
#| which is useful for syncing a subset of files with Dropbox/GDrive. |
|
#|----------------------------------------------------------------------------- |
|
# Utilities: |
|
|
|
# Parses a file name from a file path. |
|
# @param $1 - file: The file path. |
|
filename() { |
|
local _file="$1" |
|
echo "${_file%.*}" |
|
} |
|
|
|
# Parses a file extension from a file path. |
|
# @param $1 - file: The file path. |
|
extname() { |
|
local _file="$1" |
|
echo "${_file##*.}" |
|
} |
|
|
|
# Determines a file type. |
|
# @param $1 - file: The file path. |
|
# |
|
# Types: |
|
# f - File |
|
# d - Directory |
|
# l - Symlink |
|
ftype() { |
|
if [ -L "$1" ]; then |
|
echo "l" |
|
return |
|
elif [ -f "$1" ]; then |
|
echo "f" |
|
return |
|
elif [ -d "$1" ]; then |
|
echo "d" |
|
return |
|
fi |
|
|
|
echo "?" |
|
} |
|
|
|
# Converts a string to lower case. |
|
# @param $1 - string: The string. |
|
tolower() { |
|
local _string="$1" |
|
echo "$1" | tr '[:upper:]' '[:lower:]' |
|
} |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# Patterns: |
|
|
|
# Generates find(1) arguments for patterns. |
|
# |
|
# If the first argument is a "!", the function will generate arguments for |
|
# excluding the matching patterns. |
|
# |
|
# Types: |
|
# [name] - Matches file names |
|
# [name~] - Matches file names (insensitive) |
|
# [path] - Matches file paths |
|
# [path~] - Matches file paths (insensitive) |
|
# [ext] - Matches file extensions |
|
# [ext~] - Matches file extensions (insensitive) |
|
# [regex] - Matches using a regular expression. |
|
pgen() { |
|
local _negate="-o " |
|
if [ "$1" = "!" ]; then |
|
_negate="! " |
|
shift |
|
fi |
|
|
|
# Generate patterns. |
|
local patterns=() |
|
local pattern |
|
local method="-iname" |
|
local first=true |
|
for pattern in "$@"; do |
|
case "$(tolower "$pattern")" in |
|
"[path~]") |
|
method="-ipath " |
|
continue;; |
|
|
|
"[path]") |
|
method="-path " |
|
continue;; |
|
|
|
"[name~]") |
|
method="-iname " |
|
continue;; |
|
|
|
"[name]") |
|
method="-name " |
|
continue;; |
|
|
|
"[ext]") |
|
method="-name *" |
|
continue;; |
|
|
|
"[ext~]") |
|
method="-iname *" |
|
continue;; |
|
|
|
"[regex]") |
|
method="-regex " |
|
continue;; |
|
esac |
|
|
|
if $first && [ "$_negate" = "-o " ]; then |
|
first=false |
|
printf "%s%q\n" "$method" "$pattern" |
|
continue |
|
fi |
|
|
|
printf "%s%s%q\n" "$_negate" "$method" "$pattern" |
|
done |
|
} |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# Logging: |
|
|
|
log() { |
|
local COLOR="$1" |
|
local LEVEL="$2" |
|
local PATTERN="$3" |
|
local STRS=() |
|
shift 3 |
|
|
|
local entry; |
|
for entry in "$@"; do |
|
STRS+=($'\033[0m'"${entry}${COLOR}") |
|
done |
|
|
|
printf "${COLOR}${LEVEL}${PATTERN}\033[0m\n" "${STRS[@]}" |
|
} |
|
|
|
err() { |
|
log $'\033[31m' "[ERR] " "$@" |
|
} |
|
|
|
inf() { |
|
log $'\033[33m' "[INF] " "$@" |
|
} |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# Colors: |
|
|
|
MSG_RESET=$'\x1b[0m' |
|
MSG_SYS=$'\x1b[7;32m' |
|
MSG_ERR=$'\x1B[31m' |
|
MSG_CREATED=$'\x1B[33m' |
|
MSG_MOVED=$'\x1B[35m' |
|
MSG_LINKED=$'\x1B[36m' |
|
MSG_SKIPPED=$'\x1B[37m' |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# Stats: |
|
STATS_SKIPPED=0 |
|
STATS_MOVED=0 |
|
STATS_LINKED=0 |
|
STATS_ERRORS=0 |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# Symc: |
|
cb_act() { |
|
local _act="$1" |
|
local _srcfile="$2" |
|
local _destfile="$3" |
|
local _file="$4" |
|
local _cb_error="$5" |
|
shift |
|
|
|
if [ "$_act" = "skip" ]; then |
|
#log "$MSG_SKIPPED" '' "- Skipped %s." "$_file" |
|
return |
|
fi |
|
|
|
# Make directory structure: |
|
local dir="$(dirname "$_destfile")" |
|
if ! [ -d "$dir" ]; then |
|
mkdir -p "$dir" |
|
if [ $? -eq 0 ]; then |
|
log "$MSG_CREATED" '' "- Created %s." "$(dirname "$_file")" |
|
else |
|
"$_cb_error" "Could not create directory" "$@" |
|
fi |
|
fi |
|
|
|
# Take appropriate action: |
|
case "$_act" in |
|
"mv") |
|
mv "$_srcfile" "$_destfile" |
|
if [ $? -eq 0 ]; then |
|
((STATS_MOVED++)) |
|
log "$MSG_MOVED" '' "- Moved %s." "$_file" |
|
else |
|
((STATS_ERRORS++)) |
|
"$_cb_error" "Could not move file" "$@" |
|
return 1 |
|
fi |
|
;; |
|
|
|
"ln") |
|
ln -s "$_srcfile" "$_destfile" |
|
if [ $? -eq 0 ]; then |
|
((STATS_LINKED++)); |
|
log "$MSG_LINKED" '' "- Linked %s." "$_file" |
|
else |
|
((STATS_ERRORS++)); |
|
"$_cb_error" "Could not symlink file" "$@" |
|
return 1 |
|
fi |
|
;; |
|
esac |
|
} |
|
|
|
cb_error() { |
|
local _msg="$1" |
|
local _src="$2" |
|
local _dest="$3" |
|
local _file="$4" |
|
|
|
local src_type="[$(ftype "$_src")]" |
|
local dest_type="[$(ftype "$_dest")]" |
|
|
|
log "$MSG_ERR" '' "- Error: %s.\n Source: ${src_type} %s\n Destination: ${dest_type} %s" $'\x1B[1;31m'"$_msg"$'\x1B[0m' "$_src" "$_dest" |
|
} |
|
|
|
scan() { |
|
local SELF="$(basename "${BASH_SOURCE[0]}")" |
|
local _src="$1" |
|
local _dest="$2" |
|
local _cb_act="$3" |
|
local _cb_error="$4" |
|
|
|
local entry |
|
local patterns |
|
|
|
# Mover: |
|
declare -a patterns="( $(pgen "${MOVE[@]}") )" |
|
while read -r entry; do |
|
# Built-in ignore list. |
|
case "$(basename "$entry")" in |
|
.DS_Store|._*|$'Icon\r') |
|
continue;; |
|
esac |
|
|
|
# Parse file name. |
|
local file="${entry:2}" |
|
local src_file="$_src/$file" |
|
local dest_file="$_dest/$file" |
|
|
|
# Prevent overwriting. |
|
if [ -f "$src_file" ]; then |
|
"$_cb_error" "Merge conflict (dest -> src)" "$dest_file" "$src_file" "$file" |
|
continue |
|
fi |
|
|
|
# Run callback using 'mv' directive. |
|
"$_cb_act" "mv" "$dest_file" "$src_file" "$file" "$_cb_error" |
|
done < <({ cd "$_dest" && find . -depth -type f \( "${patterns[@]}" \) \( ! -path "./$SELF" ! -path "./$CONFIG" \); }) |
|
|
|
# Symlinker: |
|
declare -a patterns="( $(pgen ! "${IGNORE[@]}") )" |
|
while read -r entry; do |
|
# Parse file name. |
|
local file="${entry:2}" |
|
local src_file="$_src/$file" |
|
local dest_file="$_dest/$file" |
|
|
|
# Built-in ignore list. |
|
case "$(basename "$entry")" in |
|
.DS_Store|._*|$'Icon\r') |
|
continue;; |
|
esac |
|
|
|
# If a link already exists: |
|
# Run callback using 'skip' directive. |
|
if [ -L "$dest_file" ]; then |
|
"$_cb_act" "skip" "$src_file" "$dest_file" "$file" "$_cb_error" |
|
continue; |
|
elif [ -f "$dest_file" ]; then |
|
"$_cb_error" "Merge conflict (src -> dest)" "$dest_file" "$src_file" "$file" |
|
continue |
|
fi |
|
|
|
# Run callback using 'ln' directive. |
|
"$_cb_act" "ln" "$src_file" "$dest_file" "$file" "$_cb_error" |
|
done < <({ cd "$_src" && find . -depth -type f \( "${patterns[@]}" \) \( ! -path "./$SELF" ! -path "./$CONFIG" \); }) |
|
} |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Main: |
|
|
|
main() { |
|
local _src="$1" |
|
local _dest="$2" |
|
|
|
# Banner: |
|
local line |
|
tail -n +2 < "${BASH_SOURCE[0]}" | while read -r line; do |
|
if ! [ "${line:0:1}" = "#" ]; then |
|
break |
|
fi |
|
|
|
if [ "${line:0:2}" = "#|" ]; then |
|
printf "\x1B[33m%s\x1B[0m\n" "$(sed -e 's/^[[:space:]]*//' <<< "${line:2}")" |
|
fi |
|
done |
|
|
|
# Scan: |
|
printf "${MSG_SYS}%s${MSG_RESET}\n" "Symcing..." |
|
scan "$_src" "$_dest" "cb_act" "cb_error" |
|
printf "${MSG_SYS}%s${MSG_RESET}\n" "Symced!" |
|
|
|
# Callback: |
|
if declare -f SYMC_ON_COMPLETE > /dev/null; then |
|
SYMC_ON_COMPLETE |
|
fi |
|
} |
|
|
|
|
|
SELF="$(basename "${BASH_SOURCE[0]}")" |
|
CONFIG="$(filename "${SELF}").cfg" |
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$CONFIG" |
|
|
|
main "${SYMC_SOURCE}" "${SYMC_DESTINATION}" |