diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/common.sh | 22 | ||||
-rw-r--r-- | src/db.sh | 201 | ||||
-rw-r--r-- | src/path.sh | 94 | ||||
-rw-r--r-- | src/vars.sh | 58 |
4 files changed, 375 insertions, 0 deletions
diff --git a/src/common.sh b/src/common.sh new file mode 100644 index 0000000..18df4ce --- /dev/null +++ b/src/common.sh @@ -0,0 +1,22 @@ +# Copyright (c) 2016 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "Configuration file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +dump() { + local prefix="${FUNCNAME[0]}" + [ "${#FUNCNAME[@]}" -gt 1 ] && prefix="${FUNCNAME[1]}" + + local msg + for msg; do + echo "$prefix: $msg" + done +} + +set_dry_run() { + dry_run=1 +} + +is_dry_run() { + test -n "${dry_run+x}" +} diff --git a/src/db.sh b/src/db.sh new file mode 100644 index 0000000..6d733ef --- /dev/null +++ b/src/db.sh @@ -0,0 +1,201 @@ +# Copyright (c) 2016 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "Configuration file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Shared directory settings + +shared_dir="$( pwd )" + +update_shared_dir() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} DIR" >&2 + return 1 + fi + + local new_shared_dir + new_shared_dir="$( traverse_path --exist --directory -- "$1" )" + + [ "$db_path" == "$shared_dir/$default_db_name" ] \ + && db_path="$new_shared_dir/$default_db_name" + + shared_dir="$new_shared_dir" +} + +# Database maintenance + +readonly default_db_name='links.bin' +db_path="$shared_dir/$default_db_name" +declare -A database + +update_database_path() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} PATH" >&2 + return 1 + fi + + db_path="$( traverse_path --file -- "$1" )" + + local db_dir + db_dir="$( dirname -- "$db_path" )" + mkdir -p -- "$db_dir" +} + +ensure_database_exists() { + [ -f "$db_path" ] || is_dry_run || > "$db_path" +} + +read_database() { + [ ! -f "$db_path" ] && is_dry_run && return 0 + + local entry + while IFS= read -d '' -r entry; do + database[$entry]=1 + done < "$db_path" +} + +write_database() { + is_dry_run && return 0 + + > "$db_path" + + local entry + for entry in "${!database[@]}"; do + printf -- '%s\0' "$entry" >> "$db_path" + done +} + +delete_obsolete_dirs() { + if [ "$#" -ne 2 ]; then + echo "usage: ${FUNCNAME[0]} BASE_DIR DIR" >&2 + return 1 + fi + + is_dry_run && return 0 + + local base_dir="$1" + local dir="$2" + + base_dir="$( traverse_path --exist --directory -- "$base_dir" )" + dir="$( traverse_path --exist --directory -- "$dir" )" + + [ "$base_dir" == "$dir" ] && return 0 + + local subpath="${dir##$base_dir/}" + + if [ "$subpath" == "$dir" ]; then + dump "base directory: $base_dir" >&2 + dump "... is not a parent of: $dir" >&2 + return 1 + fi + + ( cd -- "$base_dir/" && rmdir -p --ignore-fail-on-non-empty -- "$subpath" ) +} + +delete_obsolete_entries() { + local entry + for entry in "${!database[@]}"; do + dump "entry: $entry" + unset -v 'database[$entry]' + + local var_name + var_name="$( extract_variable_name "$entry" )" || continue + cache_variable "$var_name" + + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" || continue + local shared_var_dir="$shared_dir/%$var_name%" + local subpath="${entry#%$var_name%/}" + local symlink_path="$symlink_var_dir/$subpath" + local shared_path="$shared_var_dir/$subpath" + + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + if [ ! -L "$shared_path" ] && [ ! -e "$shared_path" ]; then + dump ' the shared file is missing, so going to delete the symlink' + is_dry_run && continue + + if [ ! -L "$symlink_path" ]; then + dump " not a symlink or doesn't exist, so won't delete" + continue + fi + + local target_path + target_path="$( traverse_path -- "$symlink_path" )" + + if [ "$shared_path" != "$target_path" ]; then + dump " doesn't point to the shared file, so won't delete" + continue + fi + + rm -f -- "$symlink_path" + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + delete_obsolete_dirs "$symlink_var_dir" "$symlink_dir" || true + + continue + fi + + if [ ! -L "$symlink_path" ]; then + dump " not a symlink or doesn't exist" + continue + fi + + local target_path + target_path="$( traverse_path -- "$symlink_path" )" + + if [ "$target_path" != "$shared_path" ]; then + dump " doesn't point to the shared file" + continue + fi + + dump ' ... points to the shared file' + database[$entry]=1 + done +} + +discover_new_entries() { + local shared_var_dir + while IFS= read -d '' -r shared_var_dir; do + dump "shared directory: $shared_dir/$shared_var_dir" + + local var_name + var_name="$( extract_variable_name "$shared_var_dir" )" + cache_variable "$var_name" + + shared_var_dir="$shared_dir/$shared_var_dir" + + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" || continue + dump " symlinks directory: $symlink_var_dir" + + local shared_path + while IFS= read -d '' -r shared_path; do + dump " shared file path: $shared_path" + + local entry="%$var_name%/${shared_path:${#shared_var_dir}}" + + if [ -n "${database[$entry]+x}" ]; then + dump ' ... already has a symlink' + continue + fi + + local subpath="${shared_path:${#shared_var_dir}}" + local symlink_path="$symlink_var_dir/$subpath" + + dump " symlink path: $symlink_path" + + is_dry_run && continue + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + mkdir -p -- "$symlink_dir" + ln -f -s --no-target-directory -- "$shared_path" "$symlink_path" + + database[$entry]=1 + done < <( find "$shared_var_dir" -type f -print0 ) + + done < <( find "$shared_dir" -regextype posix-basic -mindepth 1 -maxdepth 1 -type d -regex ".*/$var_name_regex\$" -printf '%P/\0' ) +} diff --git a/src/path.sh b/src/path.sh new file mode 100644 index 0000000..b1b1e8f --- /dev/null +++ b/src/path.sh @@ -0,0 +1,94 @@ +# Copyright (c) 2016 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "Configuration file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Making sure paths point to files/directories + +_traverse_path_usage() { + local prefix="${FUNCNAME[0]}" + [ "${#FUNCNAME[@]}" -gt 1 ] && prefix="${FUNCNAME[1]}" + + local msg + for msg; do + echo "$prefix: $msg" + done + + echo "usage: $prefix [-h|--help] [-0|--null|-z|--zero] [-e|--exist] [-f|--file] [-d|--directory] [--] [PATH]..." +} + +traverse_path() { + local -a paths=() + + local must_exist= + local type_flag= + local type_name= + + local fmt='%s\n' + + while [ "$#" -gt 0 ]; do + local key="$1" + shift + + case "$key" in + -h|--help) + _traverse_path_usage + return 0 + ;; + -0|--null|-z|--zero) + fmt='%s\0' + ;; + --) + break + ;; + -e|--exist) + must_exist=1 + ;; + -d|--directory) + type_flag=-d + type_name="directory" + ;; + -f|--file) + type_flag=-f + type_name="regular file" + ;; + -*) + _traverse_path_usage "unrecognized parameter: $key" >&2 + return 1 + ;; + *) + paths+=("$key") + ;; + esac + done + + paths+=("$@") + + [ "${#paths[@]}" -eq 0 ] && return 0 + + if is_cygwin; then + local i + for i in "${!paths[@]}"; do + paths[$i]="$( cygpath -- "${paths[$i]}" )" + done + fi + + local -a abs_paths=() + + local path + while IFS= read -d '' -r path; do + if [ -n "$must_exist" ] && [ ! -e "$path" ]; then + dump "must exist: $path" >&2 + return 1 + fi + + if [ -e "$path" ] && [ -n "$type_flag" ] && ! test "$type_flag" "$path"; then + dump "must be a $type_name: $path" >&2 + return 1 + fi + + abs_paths+=("$path") + done < <( readlink -z --canonicalize-missing -- ${paths[@]+"${paths[@]}"} ) + + printf -- "$fmt" ${abs_paths[@]+"${abs_paths[@]}"} +} diff --git a/src/vars.sh b/src/vars.sh new file mode 100644 index 0000000..d74506b --- /dev/null +++ b/src/vars.sh @@ -0,0 +1,58 @@ +# Copyright (c) 2016 Egor Tensin <Egor.Tensin@gmail.com> +# This file is part of the "Configuration file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Variable resolution + +declare -A cached_paths + +resolve_variable() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} VAR_NAME" >&2 + return 1 + fi + + local var_name="$1" + + if [ -n "${cached_paths[$var_name]+x}" ]; then + echo "${cached_paths[$var_name]}" + return 0 + fi + + if [ "$var_name" = "$root_var_name" ]; then + echo '' + return 0 + fi + + if [ -z "${!var_name+x}" ]; then + dump "variable is not set: $var_name" >&2 + return 1 + fi + + local var_path="${!var_name}" + traverse_path --exist --directory -- "$var_path" +} + +cache_variable() { + local var_name + for var_name; do + [ -n "${cached_paths[$var_name]+x}" ] && continue + cached_paths[$var_name]="$( resolve_variable "$var_name" )" + done +} + +readonly root_var_name='CONFIG_LINKS_ROOT' +readonly var_name_regex='%\([_[:alpha:]][_[:alnum:]]*\)%' + +extract_variable_name() { + local s + for s; do + local var_name + if ! var_name="$( expr "$s" : "$var_name_regex/" )"; then + dump "couldn't extract variable name from: $s" >&2 + return 1 + fi + echo "$var_name" + done +} |