Compare commits

..

14 commits

12 changed files with 217 additions and 24 deletions

View file

@ -1,3 +1,7 @@
0.6.0 2024-09-03: File management with the tree strategy
File backups
Extract authorization command to top level
ask and tildify utility functions
0.5.0 2024-07-18: "Decide in editor" package conflict resolution strategy 0.5.0 2024-07-18: "Decide in editor" package conflict resolution strategy
Drop pipefail shell option for dash compatibility Drop pipefail shell option for dash compatibility
Add numerical debug levels to the log utility function Add numerical debug levels to the log utility function

View file

@ -2,7 +2,7 @@
tori is a tool to track your personal systems' configurations and replicate them. tori is a tool to track your personal systems' configurations and replicate them.
If you'd like a more detailed description of what it is, its purpose, origins and goals, see the [announcement blog post](https://blog.jutty.dev/posts/introducing-tori.html). If you'd like a more detailed description of what it is, its purpose, origins and goals, see the [announcement blog post](https://blog.jutty.dev/posts/introducing-tori/).
Refer to the [project website](https://tori.jutty.dev) for updates and access to [documentation](https://tori.jutty.dev/docs). Refer to the [project website](https://tori.jutty.dev) for updates and access to [documentation](https://tori.jutty.dev/docs).

View file

@ -6,4 +6,5 @@ check() {
log debug "collected bkp files:\n$bkp_files" log debug "collected bkp files:\n$bkp_files"
scan_packages scan_packages
merge_files "$base_files"
} }

View file

@ -15,7 +15,7 @@ scan_directory() {
done done
fi fi
echo "$files" printf "%b" "$files"
} }
scan_packages() { scan_packages() {

40
src/file/backup.sh Normal file
View file

@ -0,0 +1,40 @@
# takes a list of newline-separated absolute paths
# backs each path up, creating canonical or ephemeral copies as needed
backup_paths() {
local paths="$1"
local canonical_path=
local ephemeral_path=
for path in $paths; do
canonical_path="$BACKUP_ROOT/canonical$path"
timestamp="$(date +'%Y-%m-%dT%H-%M-%S')"
ephemeral_path="$BACKUP_ROOT/ephemeral${path}_$timestamp"
if [ -f "$canonical_path" ]; then
log debug "[backup] Creating ephemeral copy for $path"
mkdir -p "$(dirname "$ephemeral_path")"
if [ -f "$ephemeral_path" ]; then
log debug "[backup] Overwriting ephemeral copy for $path"
if [ -r "$path" ]; then
cp -f "$path" "$ephemeral_path"
else
$AUTHORIZE_COMMAND cp -f "$path" "$ephemeral_path"
fi
else
if [ -r "$path" ]; then
cp "$path" "$ephemeral_path"
else
$AUTHORIZE_COMMAND cp "$path" "$ephemeral_path"
fi
fi
else
log debug "[backup] Creating canonical copy for $path"
mkdir -p "$(dirname "$canonical_path")"
if [ -r "$path" ]; then
cp "$path" "$canonical_path"
else
$AUTHORIZE_COMMAND cp "$path" "$canonical_path"
fi
fi
done
}

85
src/file/file_merge.sh Normal file
View file

@ -0,0 +1,85 @@
merge_files() {
local base_files="$1"
local strategy="${2:-tree}"
if [ "$strategy" == tree ]; then
log info "[merge_files] Merging with $strategy strategy"
if ! file_scan_tree "$base_files"; then
if ! file_merge_tree "$base_files"; then
merge_files "$base_files"
fi
fi
fi
}
file_scan_tree() {
local base_files="$1"
for file in $base_files; do
local absolute_path="$(echo "$file" | sed 's/^base//')"
local config_path="$CONFIG_ROOT/$file"
if ! diff "$absolute_path" "$config_path" > /dev/null 2>&1; then
return 1
fi
done
return 0
}
file_merge_tree() {
local base_files="$1"
local overwrite_choice=
for file in $base_files; do
log debug "[merge_tree] Processing $file"
local absolute_path="$(echo "$file" | sed 's/^base//')"
log debug "[merge_tree] Absolute path: $absolute_path"
local config_path="$CONFIG_ROOT/$file"
log debug "[merge_tree] Config path: $config_path"
if diff "$absolute_path" "$config_path" > /dev/null 2>&1; then
log debug "[merge_tree] Files match"
else
log debug "[merge_tree] Files differ"
local prompt_verb="Differs"
local prompt_options="Overwrite system,Overwrite configuration,Show difference"
if ! [ -f "$absolute_path" ]; then
local prompt_verb="In configuration only"
local prompt_options="Copy to system"
fi
overwrite_choice="$(ask "$prompt_verb: $(tildify "$absolute_path")" "$prompt_options")"
log debug "[merge_tree] Overwrite choice: $overwrite_choice"
if [ "$overwrite_choice" -eq 0 ]; then
return 0
elif [ "$overwrite_choice" -eq 1 ]; then
backup_paths "$absolute_path"
if [ -r "$config_path" ] && [ -w "$absolute_path" ]; then
cp -vi "$config_path" "$absolute_path"
else
$AUTHORIZE_COMMAND cp -vi "$config_path" "$absolute_path"
fi
return 1
elif [ "$overwrite_choice" -eq 2 ]; then
backup_paths "$config_path"
if [ -r "$absolute_path" ] && [ -w "$config_path" ]; then
cp -vi "$absolute_path" "$config_path"
else
$AUTHORIZE_COMMAND cp -vi "$absolute_path" "$config_path"
fi
return 1
elif [ "$overwrite_choice" -eq 3 ]; then
echo "< $(tildify "$absolute_path") | $(echo "$config_path" | sed "s*$CONFIG_ROOT/**") >"
if [ -r "$absolute_path" ] && [ -r "$config_path" ]; then
diff "$absolute_path" "$config_path"
else
$AUTHORIZE_COMMAND diff "$absolute_path" "$config_path"
fi
return 1
else
log user 'Invalid choice'
return 1
fi
fi
done
}

View file

@ -9,3 +9,6 @@
. "$TORI_ROOT/src/package/validate_input_packages.sh" . "$TORI_ROOT/src/package/validate_input_packages.sh"
. "$TORI_ROOT/src/package/package_conflict_input_parser.sh" . "$TORI_ROOT/src/package/package_conflict_input_parser.sh"
. "$TORI_ROOT/src/package/update_package_cache.sh" . "$TORI_ROOT/src/package/update_package_cache.sh"
. "$TORI_ROOT/src/file/file_merge.sh"
. "$TORI_ROOT/src/file/backup.sh"

View file

@ -1,8 +1,8 @@
package_conflict_input_parser() { package_conflict_input_parser() {
local packages="$1" local packages="$1"
local conflict_type="$2" local conflict_type="$2"
local input="$TMP_DIR/package_conflict_input" local input="$TMP_ROOT/package_conflict_input"
local input_choices="$TMP_DIR/package_conflict_input_choices" local input_choices="$TMP_ROOT/package_conflict_input_choices"
local choices= local choices=
local packages_to_install= local packages_to_install=
local packages_to_uninstall= local packages_to_uninstall=

View file

@ -3,18 +3,18 @@ resolve_packages() {
local input_packages= local input_packages=
# shellcheck disable=SC2154 # shellcheck disable=SC2154
( echo "$system_packages" > "$TMP_DIR/system_packages" ( echo "$system_packages" > "$TMP_ROOT/system_packages"
echo "$user_packages" > "$TMP_DIR/user_packages" ) echo "$user_packages" > "$TMP_ROOT/user_packages" )
local packages_not_on_configuration="$(grep -v -x -f \ local packages_not_on_configuration="$(grep -v -x -f \
"$TMP_DIR/user_packages" "$TMP_DIR/system_packages" | xargs)" "$TMP_ROOT/user_packages" "$TMP_ROOT/system_packages" | xargs)"
if [ -n "$packages_not_on_configuration" ]; then if [ -n "$packages_not_on_configuration" ]; then
not_on_configuration_dialog "$packages_not_on_configuration" not_on_configuration_dialog "$packages_not_on_configuration"
fi fi
local packages_not_installed=$(grep -v -x -f \ local packages_not_installed=$(grep -v -x -f \
"$TMP_DIR/system_packages" "$TMP_DIR/user_packages" | xargs) "$TMP_ROOT/system_packages" "$TMP_ROOT/user_packages" | xargs)
if [ -n "$packages_not_installed" ]; then if [ -n "$packages_not_installed" ]; then
not_installed_dialog "$packages_not_installed" not_installed_dialog "$packages_not_installed"

View file

@ -2,7 +2,6 @@ package_manager() {
local command="$1" local command="$1"
local manager local manager
local authorizer="sudo"
local args__install local args__install
local args__uninstall local args__uninstall
local args__get_manually_installed local args__get_manually_installed
@ -25,11 +24,11 @@ package_manager() {
if [ "$command" = 'get_manually_installed' ]; then if [ "$command" = 'get_manually_installed' ]; then
eval $manager "$args__get_manually_installed" eval $manager "$args__get_manually_installed"
elif [ "$command" = 'install' ]; then elif [ "$command" = 'install' ]; then
$authorizer $manager $args__install $args__user_args $AUTHORIZE_COMMAND $manager $args__install $args__user_args
elif [ "$command" = 'uninstall' ]; then elif [ "$command" = 'uninstall' ]; then
$authorizer $manager $args__uninstall $args__user_args $AUTHORIZE_COMMAND $manager $args__uninstall $args__user_args
elif [ "$command" = 'update' ]; then elif [ "$command" = 'update' ]; then
$authorizer $manager $args__update $AUTHORIZE_COMMAND $manager $args__update
elif [ "$command" = 'get_available' ]; then elif [ "$command" = 'get_available' ]; then
eval $manager "$args__get_available" eval $manager "$args__get_available"
else else

View file

@ -5,11 +5,11 @@ log() {
local message="$2" local message="$2"
print_user_message() { print_user_message() {
echo "[tori] $(date "+%H:%M:%S"): $1" 1>&2 printf "%b\n" "[tori] $(date "+%H:%M:%S"): $1" 1>&2
} }
print_debug_message() { print_debug_message() {
echo "$(date "+%H:%M:%N") $1" 1>&2 printf "%b\n" "$(date "+%H:%M:%N") $1" 1>&2
} }
if [ -z "$DEBUG" ]; then if [ -z "$DEBUG" ]; then
@ -43,6 +43,56 @@ log() {
fi fi
} }
confirm() {
local question="$1"
local answer=
read -rp "$question [y/N] " answer
if [ "$answer" == y ] || [ "$answer" == Y ]; then
return 0;
else
return 1;
fi
}
ask() {
local question="$1"
local options="$2"
local answer=
local options_count=0
local dialog_options=
local IFS=,
for option in $options; do
_=$((options_count+=1))
dialog_options="$dialog_options\n [$options_count] $option"
done;
IFS=
dialog_options="$dialog_options\n [0] Exit"
printf "%s" "$question" >&2
printf "%b" "$dialog_options" >&2
printf "\n%s" "Choose an option number: " >&2
read -r read_answer
answer="$(echo "$read_answer" | xargs)"
if [ -z "$answer" ]; then
log info "[ask] Invalid choice"
echo -1
return 1
elif [ "$answer" -ge 0 ] 2> /dev/null && [ "$answer" -le $options_count ]; then
echo "$answer"
else
log info "[ask] Invalid choice"
echo -1
return 1
fi
}
tildify() {
echo "$1" | sed "s*$HOME*~*"
}
set_opts() { set_opts() {
local target="$1" local target="$1"
local sign= local sign=
@ -74,12 +124,22 @@ set_opts() {
} }
prepare_directories() { prepare_directories() {
if ! [ -d "$TMP_DIR" ]; then if ! [ -d "$TMP_ROOT" ]; then
mkdir "$TMP_DIR" mkdir "$TMP_ROOT"
fi fi
if ! [ -d "$CACHE_DIR" ]; then if ! [ -d "$CACHE_ROOT" ]; then
mkdir -p "$CACHE_DIR" mkdir -p "$CACHE_ROOT"
fi
if ! [ -d "$BACKUP_ROOT" ]; then
mkdir -p "$BACKUP_ROOT"
if ! [ -d "$BACKUP_ROOT/canonical" ]; then
mkdir "$BACKUP_ROOT/canonical"
fi
if ! [ -d "$BACKUP_ROOT/ephemeral" ]; then
mkdir "$BACKUP_ROOT/ephemeral"
fi
fi fi
if ! [ -d "$CONFIG_ROOT" ]; then if ! [ -d "$CONFIG_ROOT" ]; then

13
tori
View file

@ -2,11 +2,12 @@
main() { main() {
# paths # paths
VERSION="0.5.0 2024-07-18" VERSION="0.6.0 2024-09-03"
TORI_ROOT="$HOME/.local/share/tori" TORI_ROOT="$HOME/.local/share/tori"
CONFIG_ROOT="$HOME/.config/tori" CONFIG_ROOT="$HOME/.config/tori"
TMP_DIR="/tmp/tori" BACKUP_ROOT="$HOME/.local/state/tori/backup"
CACHE_DIR="$HOME/.cache/tori" TMP_ROOT="/tmp/tori"
CACHE_ROOT="$HOME/.cache/tori"
# os-independent state # os-independent state
@ -18,14 +19,14 @@ main() {
parameter="$2" parameter="$2"
# import source # import source
check_core_paths check_core_paths
. "$TORI_ROOT/src/index.sh" . "$TORI_ROOT/src/index.sh"
set_opts on
## os-dependent state ## os-dependent state
set_opts on
OS="$(get_operating_system)" OS="$(get_operating_system)"
PACKAGE_CACHE="$CACHE_DIR/${OS}_packages.cache" PACKAGE_CACHE="$CACHE_ROOT/${OS}_packages.cache"
AUTHORIZE_COMMAND="sudo"
base_files= base_files=
bkp_files= bkp_files=