287
topdir.sh
Archivo ejecutable
287
topdir.sh
Archivo ejecutable
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env bash
|
||||
# topdir.sh
|
||||
# Compara recursivamente un directorio y genera un informe de archivos nuevos y modificados
|
||||
# desde la última ejecución. Guarda el snapshot en .topdir_snapshot dentro del directorio monitoreado.
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS] [DIRECTORY]
|
||||
|
||||
Compara recursivamente un directorio y genera un informe de archivos nuevos, modificados y eliminados.
|
||||
|
||||
Opciones:
|
||||
-h, --help Muestra esta ayuda
|
||||
-s, --snapshot-file PATH
|
||||
Ruta personalizada para el archivo snapshot
|
||||
(por defecto: .topdir_snapshot dentro del DIRECTORY)
|
||||
-H, --hash ALGORITHM Algoritmo de hash: sha256 (por defecto), sha1, md5
|
||||
-f, --format FORMAT Formato de salida: text (por defecto), json, csv
|
||||
-e, --exclude PATTERN Excluir archivos/directorios que coincidan con el patrón
|
||||
(puede usarse múltiples veces, soporta wildcards)
|
||||
-i, --ignore-file FILE Leer patrones de exclusión desde un archivo
|
||||
(por defecto lee .topdirignore si existe)
|
||||
|
||||
Argumentos:
|
||||
DIRECTORY Directorio a monitorear (por defecto: directorio actual)
|
||||
|
||||
Ejemplos:
|
||||
$(basename "$0") /path/to/dir
|
||||
$(basename "$0") --hash md5 --format json .
|
||||
$(basename "$0") --exclude '*.log' --exclude 'tmp/*' /data
|
||||
$(basename "$0") --snapshot-file /tmp/my.snap --format csv /project
|
||||
EOF
|
||||
}
|
||||
|
||||
# Variables por defecto
|
||||
TARGET_DIR="."
|
||||
SNAPSHOT_FILE=""
|
||||
HASH_ALGO="sha256"
|
||||
OUTPUT_FORMAT="text"
|
||||
EXCLUDE_PATTERNS=()
|
||||
IGNORE_FILE=""
|
||||
|
||||
# Parsear opciones
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-s|--snapshot-file)
|
||||
SNAPSHOT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-H|--hash)
|
||||
HASH_ALGO="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f|--format)
|
||||
OUTPUT_FORMAT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-e|--exclude)
|
||||
EXCLUDE_PATTERNS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
-i|--ignore-file)
|
||||
IGNORE_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
echo "Error: Opción desconocida '$1'" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
TARGET_DIR="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validar algoritmo de hash
|
||||
case "$HASH_ALGO" in
|
||||
sha256|sha1|md5)
|
||||
HASH_CMD="${HASH_ALGO}sum"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Algoritmo de hash no soportado '$HASH_ALGO'. Use: sha256, sha1 o md5" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# Validar formato de salida
|
||||
case "$OUTPUT_FORMAT" in
|
||||
text|json|csv)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Formato de salida no soportado '$OUTPUT_FORMAT'. Use: text, json o csv" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "Error: '$TARGET_DIR' no es un directorio válido." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TMP_NEW=$(mktemp)
|
||||
TMP_OLD=$(mktemp)
|
||||
|
||||
# Asegurarse de limpiar archivos temporales
|
||||
cleanup() {
|
||||
rm -f "$TMP_NEW" "$TMP_OLD"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
pushd "$TARGET_DIR" >/dev/null || exit 1
|
||||
|
||||
# Definir snapshot relativo al directorio objetivo o usar el custom
|
||||
if [[ -z "$SNAPSHOT_FILE" ]]; then
|
||||
SNAPSHOT_FILE="./.topdir_snapshot"
|
||||
else
|
||||
# Si es relativo, hacerlo relativo al TARGET_DIR original
|
||||
popd >/dev/null
|
||||
if [[ "$SNAPSHOT_FILE" != /* ]]; then
|
||||
SNAPSHOT_FILE="$(pwd)/$SNAPSHOT_FILE"
|
||||
fi
|
||||
pushd "$TARGET_DIR" >/dev/null || exit 1
|
||||
fi
|
||||
|
||||
# Leer patrones de exclusión desde archivo si existe
|
||||
if [[ -n "$IGNORE_FILE" && -f "$IGNORE_FILE" ]]; then
|
||||
while IFS= read -r pattern; do
|
||||
[[ -z "$pattern" || "$pattern" == \#* ]] && continue
|
||||
EXCLUDE_PATTERNS+=("$pattern")
|
||||
done < "$IGNORE_FILE"
|
||||
elif [[ -f ".topdirignore" ]]; then
|
||||
while IFS= read -r pattern; do
|
||||
[[ -z "$pattern" || "$pattern" == \#* ]] && continue
|
||||
EXCLUDE_PATTERNS+=("$pattern")
|
||||
done < ".topdirignore"
|
||||
fi
|
||||
|
||||
# Construir comando find con exclusiones
|
||||
find_cmd=(find . -type f ! -name '.topdir_snapshot' ! -name '.topdirignore')
|
||||
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
# Convertir pattern a formato find -path
|
||||
find_cmd+=(! -path "./$pattern")
|
||||
done
|
||||
|
||||
# Generar nuevo snapshot: compute hash para cada archivo regular
|
||||
while IFS= read -r -d '' file; do
|
||||
"$HASH_CMD" "$file"
|
||||
done < <("${find_cmd[@]}" -print0 | sort -z) > "$TMP_NEW"
|
||||
|
||||
# Si no hay snapshot previo, lo creamos y salimos
|
||||
if [[ ! -f "$SNAPSHOT_FILE" ]]; then
|
||||
mv "$TMP_NEW" "$SNAPSHOT_FILE"
|
||||
|
||||
case "$OUTPUT_FORMAT" in
|
||||
text)
|
||||
echo "Snapshot creado en '$SNAPSHOT_FILE' (no había ejecución previa)."
|
||||
;;
|
||||
json)
|
||||
echo '{"status":"snapshot_created","snapshot_file":"'"$SNAPSHOT_FILE"'","new":[],"modified":[],"deleted":[]}'
|
||||
;;
|
||||
csv)
|
||||
echo "status,path"
|
||||
;;
|
||||
esac
|
||||
|
||||
popd >/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Convertir snapshot antiguo y nuevo a formato: <path>\t<checksum>
|
||||
# Convertir líneas "checksum path" a formato "path<TAB>checksum" de forma robusta
|
||||
sed -E 's/^[[:space:]]*([0-9a-f]+)[[:space:]]+(.*)$/\2\t\1/' "$SNAPSHOT_FILE" > "$TMP_OLD" 2>/dev/null || true
|
||||
sed -E 's/^[[:space:]]*([0-9a-f]+)[[:space:]]+(.*)$/\2\t\1/' "$TMP_NEW" > "$TMP_NEW.sorted"
|
||||
mv "$TMP_NEW.sorted" "$TMP_NEW"
|
||||
|
||||
# Ambos archivos ahora tienen formato: <path>\t<checksum>
|
||||
# Usar awk para detectar NEW, MOD, DEL
|
||||
new_files=()
|
||||
modified_files=()
|
||||
delete_files=()
|
||||
|
||||
awk -F"\t" '
|
||||
NR==FNR { old[$1]=$2; next }
|
||||
{
|
||||
new[$1]=$2
|
||||
if (!($1 in old)) { print "NEW\t" $1 }
|
||||
else if (old[$1] != $2) { print "MOD\t" $1 }
|
||||
delete old[$1]
|
||||
}
|
||||
END { for (p in old) print "DEL\t" p }
|
||||
' "$TMP_OLD" "$TMP_NEW" > "$TMP_OLD.changes"
|
||||
|
||||
while IFS=$'\t' read -r tag path; do
|
||||
case "$tag" in
|
||||
NEW) new_files+=("$path") ;;
|
||||
MOD) modified_files+=("$path") ;;
|
||||
DEL) delete_files+=("$path") ;;
|
||||
esac
|
||||
done < "$TMP_OLD.changes"
|
||||
|
||||
# Mostrar informe según formato
|
||||
case "$OUTPUT_FORMAT" in
|
||||
text)
|
||||
echo "Informe de comparación para: $TARGET_DIR"
|
||||
if [[ ${#new_files[@]} -eq 0 && ${#modified_files[@]} -eq 0 && ${#delete_files[@]} -eq 0 ]]; then
|
||||
echo "No se detectaron cambios desde la última ejecución."
|
||||
else
|
||||
if [[ ${#new_files[@]} -gt 0 ]]; then
|
||||
echo -e "\nNuevos archivos (${#new_files[@]}):"
|
||||
for f in "${new_files[@]}"; do
|
||||
echo " $f"
|
||||
done
|
||||
fi
|
||||
if [[ ${#modified_files[@]} -gt 0 ]]; then
|
||||
echo -e "\nArchivos modificados (${#modified_files[@]}):"
|
||||
for f in "${modified_files[@]}"; do
|
||||
echo " $f"
|
||||
done
|
||||
fi
|
||||
if [[ ${#delete_files[@]} -gt 0 ]]; then
|
||||
echo -e "\nArchivos eliminados desde último snapshot (${#delete_files[@]}):"
|
||||
for f in "${delete_files[@]}"; do
|
||||
echo " $f"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
json)
|
||||
# Construir arrays JSON
|
||||
new_json="["
|
||||
for i in "${!new_files[@]}"; do
|
||||
[[ $i -gt 0 ]] && new_json+=","
|
||||
# Escapar comillas en el path
|
||||
escaped_path="${new_files[$i]//\"/\\\"}"
|
||||
new_json+="\"$escaped_path\""
|
||||
done
|
||||
new_json+="]"
|
||||
|
||||
mod_json="["
|
||||
for i in "${!modified_files[@]}"; do
|
||||
[[ $i -gt 0 ]] && mod_json+=","
|
||||
escaped_path="${modified_files[$i]//\"/\\\"}"
|
||||
mod_json+="\"$escaped_path\""
|
||||
done
|
||||
mod_json+="]"
|
||||
|
||||
del_json="["
|
||||
for i in "${!delete_files[@]}"; do
|
||||
[[ $i -gt 0 ]] && del_json+=","
|
||||
escaped_path="${delete_files[$i]//\"/\\\"}"
|
||||
del_json+="\"$escaped_path\""
|
||||
done
|
||||
del_json+="]"
|
||||
|
||||
echo "{\"status\":\"ok\",\"directory\":\"$TARGET_DIR\",\"hash\":\"$HASH_ALGO\",\"new\":$new_json,\"modified\":$mod_json,\"deleted\":$del_json}"
|
||||
;;
|
||||
|
||||
csv)
|
||||
echo "status,path"
|
||||
for f in "${new_files[@]}"; do
|
||||
echo "new,\"$f\""
|
||||
done
|
||||
for f in "${modified_files[@]}"; do
|
||||
echo "modified,\"$f\""
|
||||
done
|
||||
for f in "${delete_files[@]}"; do
|
||||
echo "deleted,\"$f\""
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
# Actualizar snapshot atómico
|
||||
mv "$TMP_NEW" "$SNAPSHOT_FILE"
|
||||
|
||||
popd >/dev/null || true
|
||||
exit 0
|
||||
Referencia en una nueva incidencia
Block a user