When adding files or directory in a .delete directory that file or folder will be removed
272 lines
6.4 KiB
Bash
Executable File
272 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
src_dir="$script_dir/config"
|
|
dest_dir="${HOME}/.config"
|
|
|
|
|
|
dry_run=false
|
|
target_path=""
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: sync-config.sh [--dry-run] [path]
|
|
|
|
path can be:
|
|
config (full config)
|
|
config/tmux (specific folder)
|
|
config/tmux/config (specific file)
|
|
.config (full config)
|
|
.config/tmux (specific folder)
|
|
.config/tmux/config (specific file)
|
|
tmux (folder under .config)
|
|
tmux/config (file under .config)
|
|
EOF
|
|
}
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run)
|
|
dry_run=true
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
if [[ -z "$target_path" ]]; then
|
|
target_path="$arg"
|
|
else
|
|
echo "Error: too many arguments." >&2
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
hash_cmd="sha256sum"
|
|
if ! command -v "$hash_cmd" >/dev/null 2>&1; then
|
|
echo "Error: sha256sum not found. Install coreutils or adjust hash_cmd." >&2
|
|
exit 1
|
|
fi
|
|
|
|
file_hash() {
|
|
"$hash_cmd" "$1" | awk '{print $1}'
|
|
}
|
|
|
|
copy_file() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
if $dry_run; then
|
|
echo "copy: $src -> $dst"
|
|
else
|
|
mkdir -p "$(dirname "$dst")"
|
|
cp -a "$src" "$dst"
|
|
echo "copied: $dst"
|
|
fi
|
|
}
|
|
|
|
delete_file() {
|
|
local dst="$1"
|
|
if $dry_run; then
|
|
echo "delete: $dst"
|
|
else
|
|
rm -f "$dst"
|
|
echo "deleted: $dst"
|
|
fi
|
|
}
|
|
|
|
delete_dir() {
|
|
local dst="$1"
|
|
if $dry_run; then
|
|
echo "delete dir: $dst"
|
|
else
|
|
rm -rf "$dst"
|
|
echo "deleted dir: $dst"
|
|
fi
|
|
}
|
|
|
|
if [[ ! -d "$src_dir" ]]; then
|
|
echo "Error: source config directory not found: $src_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$dest_dir"
|
|
|
|
target_rel="$target_path"
|
|
if [[ -n "$target_rel" ]]; then
|
|
if [[ "$target_rel" == "config" || "$target_rel" == ".config" ]]; then
|
|
target_rel=""
|
|
elif [[ "$target_rel" == config/* ]]; then
|
|
target_rel=${target_rel#config/}
|
|
elif [[ "$target_rel" == .config/* ]]; then
|
|
target_rel=${target_rel#.config/}
|
|
fi
|
|
target_rel=${target_rel#./}
|
|
if [[ "$target_rel" == "." ]]; then
|
|
target_rel=""
|
|
fi
|
|
if [[ "$target_rel" == /* ]]; then
|
|
echo "Error: absolute paths are not supported: $target_path" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
base_path="$src_dir"
|
|
if [[ -n "$target_rel" ]]; then
|
|
base_path="$src_dir/$target_rel"
|
|
fi
|
|
|
|
if [[ ! -e "$base_path" ]]; then
|
|
echo "Error: path not found in source: $base_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
check_delete_marker() {
|
|
local rel_path="$1"
|
|
|
|
# Check centralized .delete folder: config/.delete/path/to/dir_or_file
|
|
if [[ -f "$src_dir/.delete/$rel_path" ]] || [[ -d "$src_dir/.delete/$rel_path" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check inline .delete marker: config/path/to/.delete/dir_or_file
|
|
local parent_path="$src_dir/$rel_path"
|
|
local parent_dir=$(dirname "$parent_path")
|
|
local base_name=$(basename "$parent_path")
|
|
if [[ -f "$parent_dir/.delete/$base_name" ]] || [[ -d "$parent_dir/.delete/$base_name" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if a path is a .delete directory or inside one (should be skipped during traversal)
|
|
is_in_delete_dir() {
|
|
local rel_path="$1"
|
|
# Check if the path itself is a .delete directory or any parent is .delete
|
|
local current="$rel_path"
|
|
while [[ "$current" != "." && "$current" != "/" && -n "$current" ]]; do
|
|
local base
|
|
base=$(basename "$current")
|
|
if [[ "$base" == ".delete" ]]; then
|
|
return 0
|
|
fi
|
|
current=$(dirname "$current")
|
|
done
|
|
return 1
|
|
}
|
|
|
|
sync_file() {
|
|
local src_path="$1"
|
|
local rel_path=${src_path#"$src_dir"/}
|
|
local dst_path="$dest_dir/$rel_path"
|
|
|
|
# Check if there's a corresponding .delete marker for this file
|
|
if check_delete_marker "$rel_path"; then
|
|
if [[ -f "$dst_path" ]]; then
|
|
delete_file "$dst_path"
|
|
deleted_files+=("$dst_path")
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if [[ ! -f "$dst_path" ]]; then
|
|
copy_file "$src_path" "$dst_path"
|
|
return
|
|
fi
|
|
|
|
local src_hash
|
|
local dst_hash
|
|
src_hash=$(file_hash "$src_path")
|
|
dst_hash=$(file_hash "$dst_path")
|
|
|
|
if [[ "$src_hash" != "$dst_hash" ]]; then
|
|
copy_file "$src_path" "$dst_path"
|
|
fi
|
|
}
|
|
|
|
shopt -s globstar nullglob dotglob
|
|
if [[ -f "$base_path" ]]; then
|
|
sync_file "$base_path"
|
|
elif [[ -d "$base_path" ]]; then
|
|
for src_path in "$base_path"/**; do
|
|
rel_path=${src_path#"$src_dir"/}
|
|
|
|
if [[ "$rel_path" == ".git" || "$rel_path" == .git/* ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Check if this is a .delete directory - process its contents to delete in destination
|
|
if [[ -d "$src_path" && "$(basename "$rel_path")" == ".delete" ]]; then
|
|
# Get the parent directory of the .delete folder
|
|
parent_rel_path=$(dirname "$rel_path")
|
|
# Process all files in this .delete directory to delete corresponding files in destination
|
|
for delete_marker in "$src_path"/**; do
|
|
if [[ -e "$delete_marker" ]]; then
|
|
# Get the basename of the marker (file/dir to delete)
|
|
marker_basename=$(basename "$delete_marker")
|
|
# Construct the target path in the parent directory
|
|
target_rel_path="$parent_rel_path/$marker_basename"
|
|
target_dst_path="$dest_dir/$target_rel_path"
|
|
|
|
if [[ -d "$delete_marker" ]]; then
|
|
if [[ -d "$target_dst_path" ]]; then
|
|
delete_dir "$target_dst_path"
|
|
fi
|
|
elif [[ -f "$delete_marker" ]]; then
|
|
if [[ -f "$target_dst_path" ]]; then
|
|
delete_file "$target_dst_path"
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
# Delete the .delete folder in destination if it exists
|
|
delete_dst_path="$dest_dir/$rel_path"
|
|
if [[ -d "$delete_dst_path" ]]; then
|
|
delete_dir "$delete_dst_path"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
# Skip anything inside a .delete directory - don't process or copy
|
|
if is_in_delete_dir "$rel_path"; then
|
|
continue
|
|
fi
|
|
|
|
dst_path="$dest_dir/$rel_path"
|
|
|
|
if [[ -d "$src_path" ]]; then
|
|
# Check if there's a .delete marker for this directory
|
|
if check_delete_marker "$rel_path"; then
|
|
if [[ -d "$dst_path" ]]; then
|
|
delete_dir "$dst_path"
|
|
deleted_dirs+=("$dst_path")
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if ! $dry_run; then
|
|
mkdir -p "$dst_path"
|
|
else
|
|
echo "mkdir: $dst_path"
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [[ ! -f "$src_path" ]]; then
|
|
continue
|
|
fi
|
|
|
|
sync_file "$src_path"
|
|
done
|
|
fi
|
|
|
|
if $dry_run; then
|
|
echo "Dry run complete. Re-run without --dry-run to apply changes."
|
|
else
|
|
echo "Sync complete. Updated files where content differed."
|
|
fi
|