diff options
| author | Egor Tensin <egor@tensin.name> | 2026-06-06 18:12:33 +0200 |
|---|---|---|
| committer | Egor Tensin <egor@tensin.name> | 2026-06-06 18:17:34 +0200 |
| commit | 40b7b047a3fbf6451228de7f69c8ad5f156f7d1c (patch) | |
| tree | 365df06d7b7cb6122d8a30695aa45a12c4f58302 /lib/db.sh | |
| parent | workflows/ci: remove on: pull_request (diff) | |
| download | config-links-2.1.0.tar.gz config-links-2.1.0.zip | |
reorganize filesv2.1.0
Diffstat (limited to 'lib/db.sh')
| -rw-r--r-- | lib/db.sh | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/lib/db.sh b/lib/db.sh new file mode 100644 index 0000000..ae96487 --- /dev/null +++ b/lib/db.sh @@ -0,0 +1,308 @@ +# Copyright (c) 2016 Egor Tensin <egor@tensin.name> +# This file is part of the "Config file sharing" project. +# For details, see https://github.com/egor-tensin/config-links. +# Distributed under the MIT License. + +# Shared directory settings + +shared_root_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" )" + + [ "$new_shared_dir" = / ] || new_shared_dir="$new_shared_dir/" + + [ "$db_path" = "$shared_root_dir$default_db_name" ] \ + && db_path="$new_shared_dir$default_db_name" + + shared_root_dir="$new_shared_dir" +} + +# Database maintenance + +readonly default_db_name='links.bin' +db_path="$shared_root_dir$default_db_name" +declare -A database=() +declare -A shared_paths=() +declare -A symlink_paths=() + +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" +} + +add_entry() { + local entry + for entry; do + dump "entry: $entry" + + local var_name + var_name="$( extract_variable_name "$entry" )" + cache_variable "$var_name" + + local shared_var_dir="$shared_root_dir%$var_name%" + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" + local subpath="${entry#%"$var_name"%/}" + + local shared_path="$shared_var_dir" + [ "$shared_var_dir" != / ] && shared_path="$shared_path/" + shared_path="$shared_path$subpath" + + local symlink_path="$symlink_var_dir" + [ "$symlink_var_dir" != / ] && symlink_path="$symlink_path/" + symlink_path="$symlink_path$subpath" + + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + database[$entry]="$var_name" + shared_paths[$entry]="$shared_path" + symlink_paths[$entry]="$symlink_path" + done +} + +remove_entry() { + local entry + for entry; do + unset -v 'database[$entry]' + unset -v 'shared_paths[$entry]' + unset -v 'symlink_paths[$entry]' + done +} + +read_database() { + [ ! -r "$db_path" ] && return 0 + + local entry + while IFS= read -d '' -r entry; do + add_entry "$entry" + done < "$db_path" +} + +write_database() { + is_dry_run && return 0 + + [ "${#database[@]}" -eq 0 ] && rm -f -- "$db_path" && return 0 + + > "$db_path" + + local entry + for entry in "${!database[@]}"; do + printf -- '%s\0' "$entry" >> "$db_path" + done +} + +link_entry() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + mkdir -p -- "$symlink_dir" + ln -f -s --no-target-directory -- "$shared_path" "$symlink_path" + done +} + +delete_obsolete_dirs() { + if [ "$#" -ne 2 ]; then + echo "usage: ${FUNCNAME[0]} BASE_DIR DIR" >&2 + return 1 + fi + + 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" ) +} + +unlink_entry() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + + rm -f -- "$symlink_path" + + local symlink_dir + symlink_dir="$( dirname -- "$symlink_path" )" + local var_name="${database[$entry]}" + local symlink_var_dir + symlink_var_dir="$( resolve_variable "$var_name" )" + delete_obsolete_dirs "$symlink_var_dir" "$symlink_dir" || true + done +} + +symlink_present() { + local entry + for entry; do + local symlink_path="${symlink_paths[$entry]}" + test -L "$symlink_path" -o -e "$symlink_path" + done +} + +symlink_points_to_shared_file() { + symlink_present "$@" + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + shared_path="$( traverse_path -- "$shared_path" )" + local symlink_path="${symlink_paths[$entry]}" + local target_path + target_path="$( traverse_path -- "$symlink_path" )" + test "$target_path" = "$shared_path" + done +} + +shared_file_present() { + local entry + for entry; do + local shared_path="${shared_paths[$entry]}" + test -L "$shared_path" -o -e "$shared_path" + done +} + +link_all_entries() { + local -a shared_var_dirs=() + local shared_var_dir + + find "$shared_root_dir" \ + -mindepth 1 -maxdepth 1 \ + -\( -type d -o -type l -\) \ + -regextype posix-basic \ + -regex ".*/$var_name_regex\$" \ + -printf '%P\0' | + while IFS= read -d '' -r shared_var_dir; do + shared_var_dirs+=("$shared_var_dir") + done + + for shared_var_dir in ${shared_var_dirs[@]+"${shared_var_dirs[@]}"}; do + dump "shared directory: $shared_root_dir$shared_var_dir" + local shared_path + + find "$shared_root_dir$shared_var_dir/" -\( -type f -o -type l -\) -print0 | + while IFS= read -d '' -r shared_path; do + dump " shared file path: $shared_path" + local entry="${shared_path:${#shared_root_dir}}" + add_entry "$entry" > /dev/null + dump " symlink path: ${symlink_paths[$entry]}" + + if symlink_present "$entry"; then + if symlink_points_to_shared_file "$entry"; then + dump ' ... up-to-date' + else + dump " ... not a symlink or doesn't point to the shared file, adding a symlink" + is_dry_run || link_entry "$entry" + fi + else + dump ' ... adding a symlink' + is_dry_run || link_entry "$entry" + fi + done + done +} + +unlink_all_entries() { + local entry + for entry in "${!database[@]}"; do + dump "entry: $entry" + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + if symlink_points_to_shared_file "$entry"; then + dump ' ... removing the symlink' + is_dry_run || unlink_entry "$entry" + remove_entry "$entry" + else + dump " ... not a symlink or doesn't point to the shared file" + remove_entry "$entry" + fi + done +} + +unlink_obsolete_entries() { + local entry + for entry in "${!database[@]}"; do + dump "entry: $entry" + local shared_path="${shared_paths[$entry]}" + local symlink_path="${symlink_paths[$entry]}" + dump " shared file path: $shared_path" + dump " symlink path: $symlink_path" + + if symlink_points_to_shared_file "$entry"; then + if shared_file_present "$entry"; then + dump ' ... up-to-date' + else + dump ' ... obsolete' + is_dry_run || unlink_entry "$entry" + remove_entry "$entry" + fi + else + dump " ... not a symlink or doesn't point to the shared file" + remove_entry "$entry" + fi + done +} + +parse_mode() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} MODE" >&2 + return 1 + fi + + local mode="$1" + shift + + if [ -z "$mode" ]; then + dump "mode cannot be empty" >&2 + return 1 + fi + + echo "$mode" +} + +chmod_entries() { + if [ "$#" -ne 1 ]; then + echo "usage: ${FUNCNAME[0]} MODE" >&2 + return 1 + fi + + local mode="$1" + shift + + dump "applying mode $mode to shared files..." + if [ "${#shared_paths[@]}" -ne 0 ]; then + is_dry_run || chmod -- "$mode" ${shared_paths[@]+"${shared_paths[@]}"} + fi +} |
