#!/usr/bin/env bash # prepare_chrome_profile — clona un user-data-dir de Chrome/Chromium conservando solo # las extensiones de una lista blanca. Sirve para perfiles de scraping limpios. set -euo pipefail # ── defaults ────────────────────────────────────────────────────────────────── _SRC="" _DST="" _FORCE=0 # uBlock Origin Lite por defecto _KEEP=() _DEFAULT_EXT="ddkjiahejlhfcafbddmgiahcphecmpfh" # ── parse args ──────────────────────────────────────────────────────────────── _usage() { cat >&2 <<'EOF' Usage: prepare_chrome_profile --src --dst \ [--keep ]... [--force] --src user-data-dir origen (ej. $HOME/.config/chromium) --dst user-data-dir destino a crear --keep ID de extensión a conservar (repetible). Default: uBlock Origin Lite --force si --dst existe, lo borra y recrea; sin flag aborta si existe Exit codes: 0 éxito 1 error de argumento o validación 2 --dst ya existe y no se pasó --force 3 --src igual a --dst (mismo path real) 4 error de copia/rsync EOF exit 1 } while [[ $# -gt 0 ]]; do case "$1" in --src) _SRC="$2"; shift 2 ;; --dst) _DST="$2"; shift 2 ;; --keep) _KEEP+=("$2"); shift 2 ;; --force) _FORCE=1; shift ;; -h|--help) _usage ;; *) echo "prepare_chrome_profile: argumento desconocido: $1" >&2; _usage ;; esac done # ── validaciones básicas ────────────────────────────────────────────────────── if [[ -z "$_SRC" || -z "$_DST" ]]; then echo "prepare_chrome_profile: --src y --dst son obligatorios" >&2 exit 1 fi if [[ ! -d "$_SRC/Default" ]]; then echo "prepare_chrome_profile: $_SRC/Default no existe; ¿es un user-data-dir válido?" >&2 exit 1 fi # Resolver paths reales para comparar (evitar borrar src cuando src==dst) _SRC_REAL="$(realpath "$_SRC")" _DST_REAL="$(realpath -m "$_DST")" # -m: no requiere que exista if [[ "$_SRC_REAL" == "$_DST_REAL" ]]; then echo "prepare_chrome_profile: --src y --dst resuelven al mismo path: $_SRC_REAL" >&2 exit 3 fi # También rechazar si --dst es prefijo de --src (evitar borrar el origen) if [[ "$_SRC_REAL" == "$_DST_REAL"/* ]]; then echo "prepare_chrome_profile: --src está dentro de --dst; operación peligrosa, abortando" >&2 exit 3 fi # ── lista blanca de extensiones ─────────────────────────────────────────────── if [[ ${#_KEEP[@]} -eq 0 ]]; then _KEEP=("$_DEFAULT_EXT") fi # ── gestionar destino ───────────────────────────────────────────────────────── if [[ -d "$_DST" ]]; then if [[ $_FORCE -eq 1 ]]; then rm -rf "$_DST" else echo "prepare_chrome_profile: $_DST ya existe; usa --force para sobreescribir" >&2 exit 2 fi fi mkdir -p "$_DST/Default" # ── copiar Local State (HMAC seed para Secure Preferences) ──────────────────── if [[ -f "$_SRC/Local State" ]]; then cp "$_SRC/Local State" "$_DST/Local State" fi # ── rsync del perfil Default excluyendo caché y locks ───────────────────────── rsync -a \ --exclude='Cache/' \ --exclude='Code Cache/' \ --exclude='GPUCache/' \ --exclude='Dawn Cache/' \ --exclude='DawnGraphiteCache/' \ --exclude='DawnWebGPUCache/' \ --exclude='Service Worker/CacheStorage/' \ --exclude='Service Worker/ScriptCache/' \ --exclude='Singleton*' \ --exclude='*.lock' \ --exclude='lockfile' \ --exclude='Sessions/' \ --exclude='Session Storage/' \ --exclude='Current Session' \ --exclude='Current Tabs' \ --exclude='Last Session' \ --exclude='Last Tabs' \ "$_SRC/Default/" "$_DST/Default/" || { echo "prepare_chrome_profile: rsync falló (exit $?)" >&2 exit 4 } # ── eliminar extensiones fuera de la lista blanca ──────────────────────────── _EXT_DIR="$_DST/Default/Extensions" _removed=() _kept=() if [[ -d "$_EXT_DIR" ]]; then while IFS= read -r -d '' ext_path; do ext_id="$(basename "$ext_path")" # Conservar siempre la carpeta Temp (usada por Chrome durante installs) if [[ "$ext_id" == "Temp" ]]; then continue fi # Comprobar si está en la lista blanca _in_keep=0 for keep_id in "${_KEEP[@]}"; do if [[ "$ext_id" == "$keep_id" ]]; then _in_keep=1 break fi done if [[ $_in_keep -eq 1 ]]; then _kept+=("$ext_id") else rm -rf "$ext_path" _removed+=("$ext_id") fi done < <(find "$_EXT_DIR" -mindepth 1 -maxdepth 1 -type d -print0) fi # ── purgar referencias a extensiones eliminadas en Preferences ─────────────── # Chrome re-descarga del Web Store cualquier extensión que aparezca en # extensions.settings aunque su carpeta haya sido borrada. Editamos el JSON # con python3 para evitar ese comportamiento. if [[ ${#_removed[@]} -gt 0 ]]; then # Construir lista Python de IDs eliminados _py_ids_list="" for _id in "${_removed[@]}"; do _py_ids_list+="\"${_id}\"," done _py_ids_list="[${_py_ids_list%,}]" for _prefs_file in "$_DST/Default/Preferences" "$_DST/Default/Secure Preferences"; do if [[ -f "$_prefs_file" ]]; then python3 - "$_prefs_file" "$_py_ids_list" <<'PY' || \ echo "prepare_chrome_profile: advertencia — no se pudieron purgar refs en $(basename "$_prefs_file")" >&2 import sys, json prefs_path = sys.argv[1] removed_ids = json.loads(sys.argv[2]) with open(prefs_path, "r", encoding="utf-8") as f: data = json.load(f) # 1. extensions.settings. ext_settings = data.get("extensions", {}).get("settings", {}) for ext_id in removed_ids: ext_settings.pop(ext_id, None) # 2. extensions.pinned_extensions (lista de IDs) pinned = data.get("extensions", {}).get("pinned_extensions", None) if isinstance(pinned, list): data["extensions"]["pinned_extensions"] = [ pid for pid in pinned if pid not in removed_ids ] # 3. protection.macs.extensions.settings. (Secure Preferences) try: mac_ext = data["protection"]["macs"]["extensions"]["settings"] for ext_id in removed_ids: mac_ext.pop(ext_id, None) except (KeyError, TypeError): pass with open(prefs_path, "w", encoding="utf-8") as f: json.dump(data, f, separators=(",", ":")) PY fi done fi # ── emitir resultado JSON ───────────────────────────────────────────────────── _json_array() { # Convierte array bash en JSON array de strings local arr=("$@") local out="[" local first=1 for item in "${arr[@]}"; do if [[ $first -eq 1 ]]; then out+="\"$item\"" first=0 else out+=",\"$item\"" fi done out+="]" echo "$out" } _kept_json="$(_json_array "${_kept[@]+"${_kept[@]}"}")" _removed_json="$(_json_array "${_removed[@]+"${_removed[@]}"}")" printf '{"dst":"%s","kept":%s,"removed":%s}\n' \ "$_DST_REAL" \ "$_kept_json" \ "$_removed_json"