viernes, 17 de octubre de 2025

WEBPROPv2 (XIV). Ampliar las funcionalidades. Insalar libreoofice y unoconv

 1. Introducción

Para poder visualizar ficheros se requiere tener un conversor de ficheros doc, docx, xls ,odt ... a html para poderlos visualizar por tanto se requiere que:

  1. Se instale unoconv y libreoffice
  2. Crear el servico de libreoffice para que sea mas rápida la conversión
  3. Ejecutar el servicio
Para instalr estos ficheros en Ubuntu (ojo la opción --fix-missing es por si no puede descargar algun paquete):

sudo apt update --fix-missing
sudo apt install unoconv libreoffice --fix-missing

Y para crear  el servicio:

Ejecutamos:

sudo nano /etc/systemd/system/libreoffice-listener.service


Y le damos este contenido:

[Unit]
Description=LibreOffice headless listener for unoconv
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/libreoffice --headless --nologo --nofirststartwizard  --norestore --accept="socket,host=127.0.0.1,port=2002;urp;"
Restart=always
RestartSec=5

# Opcional: ejecutar como un usuario no root
User=libreoffice
Environment="HOME=/tmp"

[Install]
WantedBy=multi-user.target


Y como lo ejecutamos como el usuario libreoffice, tenemos que crearlo:

sudo useradd -r -s /bin/false libreoffice
sudo mkdir /tmp/libreoffice
sudo chown libreoffice:libreoffice /tmp/libreoffice

Ahora toca arrancar el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now libreoffice-listener.service


Y verificamos que esté ctivo el servicio:

systemctl status libreoffice-listener.service

Y que funcione la conversión:

unoconv -f html -d document test.docx


que crea el fichero test,html al convertir el documento (existente) text.docx


2. Problemas

Hay que instalr 






lunes, 13 de octubre de 2025

WEBPROPv2 (XIII). Tareas de manternimiento. Instalar librerías.

 1. Instalar nuevas dependencias en el entorno virtual

Primeramente habrá que saber que dependencias instalar. Para ello nos vamos al entorno virtual de la máquina de desarrollo y creamos el fichero "requirements.txt"

1
2
3
4
5
6
7
8
# 1. Primeramente vamos a la carpeta que contiene la carpeta del entorno virtual
cd softprop

#2. Activamos el entrono que esta en nuestro caso en venv_softprop
source venv_softprop/bin/activate

#3. Creamos el fichero de la lista de librerías
pip freeze > requirements.txt


Ahoea hay que copiar el fichero "requirements.txt" en el host remoto de produccón a la carpeta padre que contiene la carpeta ddel entrono virtual. En mi caso lo he hecho con Filezilla

Nos conectamos con ssh con el servidor remoto de producción y hacemos la misma operación pero al revés:

1
2
3
4
5
6
7
8
# 1. Primeramente vamos a la carpeta que contiene la carpeta del entorno virtual
cd softprop

#2. Activamos el entrono que esta en nuestro caso en venv_softprop
source venv_softprop/bin/activate

#3. Creamos el fichero de la lista de librerías
pip install -r requirements.txt





 


domingo, 12 de octubre de 2025

COPIAR CARPETAS pero evitando duplicados internos y excluyendo algunas carpetas y extensiones de ficheros

 Introducción

Lo vamos a hacer en 2 pasos:

  1. Detectaremos duplicados por nombre y tamaño (excluyendo algunas carpetas y extensiones de ficheros) creando un fichero con la lista de duplicados
  2. Copiaremos los ficheros excluyendo los ficheros de la lista guadada en el fichero generado anteriormente y las carpetas y ficheros excluidos por extensión

1. Generar una lista de ficheros duplicados:


  • Las líneas 35 a 39 indican las carpetas a excluir
  • Las lÍneas 42 a 44 indica las extensiones de ficheros a excluir
  • Al final nos da el nombre del fichero generado
  • Para ejecutar el script hay que pasarle la carpeta a analizar
Ejecutamos este  script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/bin/bash
# buscar_duplicados_exacto_final.sh
# Autor: Ximo Dante con IA
# Busca archivos con el mismo nombre y tamaño exacto,
# excluyendo carpetas del sistema y archivos temporales.
# Marca con "#" las cabeceras y el fichero más superficial.

set -euo pipefail
IFS=$'\n'

BASE_DIR="${1:-}"

if [[ -z "$BASE_DIR" ]]; then
  echo "❌ Uso: $0 /ruta/a/carpeta"
  exit 1
fi
if [[ ! -d "$BASE_DIR" ]]; then
  echo "❌ No existe el directorio: $BASE_DIR"
  exit 1
fi

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUT="duplicados_final_${TIMESTAMP}.txt"

echo "🔍 Buscando duplicados exactos en: $BASE_DIR"
echo "📝 Informe: $OUT"
echo

# ------------------------------------------------------------
# 1️⃣ Buscar y procesar con find + awk (sin bucles)
# ------------------------------------------------------------
find "$BASE_DIR" \
  \( -type d \( \
      -name ".*" -o -name "\$*" \
      -o -iname "Archivos de programa" -o -iname "Descargas" -o -iname "Downloads" \
      -o -iname "Documents and Settings" -o -iname "Logs" -o -iname "PerfLogs" \
      -o -iname "Program Files" -o -iname "Program Files (x86)" -o -iname "ProgramData" \
      -o -iname "Recovery" -o -iname "System Volume Information" -o -iname "Temp" \
      -o -iname "Users" -o -iname "Windows" -o -iname "Windows10Upgrade" \
    \) -prune \) \
  -o -type f \
     ! -iname "*.exe" ! -iname "*.log" ! -iname "*.dll" ! -iname "*.sys"\
     ! -iname "*.part" ! -iname "*.crdownload" ! -iname "*.tmp" \
     ! -iname "*.partial" ! -iname "*.download" ! -iname "*!qB*" ! -iname "*.aria2" \
  -printf '%f\t%s\t%p\n' 2>/dev/null \
| sort -k1,1 -k2,2n \
| awk -F'\t' -v base="$BASE_DIR" '
  function depth(path,   nf, nb, a, b) {
    nf = split(path, a, "/");
    nb = split(base, b, "/");
    return nf - nb - 1;
  }

  {
    name=$1; size=$2; path=$3;
    # Ignorar temporales (~$, _~, ~.)
    if (name ~ /^~[$]|^_~|^~\./) next;
    key = name "|" size;
    files[key] = files[key] ? files[key] RS path : path;
  }
  END {
    dup_groups=0;
    for (k in files) {
      n = split(files[k], arr, "\n");
      if (n > 1) {
        dup_groups++;
        split(k, parts, "|");
        print "#-----------------------------------";
        print "#📂 Duplicado exacto: " parts[1] " (" parts[2] " bytes)";
        # Fichero más superficial (menor profundidad)
        min_d=1e9; idx=0;
        for (i=1; i<=n; i++) {
          d = depth(arr[i]);
          depths[i]=d;
          if (d < min_d) { min_d=d; idx=i; }
        }
        for (i=1; i<=n; i++) {
          mark = (i==idx) ? "#" : " ";
          printf "%s%s [nivel=%d]\n", mark, arr[i], depths[i];
        }
      }
    }
    if (dup_groups==0)
      print "ℹ️ No se encontraron duplicados exactos (nombre + tamaño)" > "/dev/stderr";
    else
      print "✅ Se encontraron " dup_groups " grupos de duplicados." > "/dev/stderr";
  }
' > "$OUT"

echo
if [[ -s "$OUT" ]]; then
  echo "✅ Informe generado: $(realpath "$OUT")"
else
  echo "ℹ️ El fichero está vacío: no se detectaron duplicados exactos tras exclusiones."
fi
echo


2. Copia de carpetas en base al fichero de duplicados:

  • Las líneas 55 a 59 indican las carpetas a excluir
  • Las lÍneas 62 a 64 indica las extensiones de ficheros a excluir
  • Al final nos da el nombre del fichero generado con la lista de ficheros copiados
  • Para ejecutar el script hay que pasarle las carpeta de origen y destino y la ruta del fichero de duplicados generados en el punto anterior
Veamos el script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/bin/bash
# ============================================================
# 🧰 copiar_no_duplicados.sh
# Autor: Ximo Dante + IA
# Copia solo los ficheros no duplicados ni excluidos
# según el informe generado por buscar_duplicados_exacto_final.sh
# Sin usar rsync — usa find + cp --parents (más estable)
# ============================================================

set -euo pipefail
IFS=$'\n'

ORIGEN="${1:-}"
DESTINO="${2:-}"
DUP_FILE="${3:-}"

if [[ -z "$ORIGEN" || -z "$DESTINO" || -z "$DUP_FILE" ]]; then
  echo "❌ Uso: $0 /carpeta/origen /carpeta/destino duplicados_final_xxx.txt"
  exit 1
fi

if [[ ! -d "$ORIGEN" ]]; then
  echo "❌ Carpeta origen no existe: $ORIGEN"
  exit 1
fi
if [[ ! -f "$DUP_FILE" ]]; then
  echo "❌ No existe el fichero de duplicados: $DUP_FILE"
  exit 1
fi

mkdir -p "$DESTINO"
TS=$(date +%Y%m%d_%H%M%S)
LOG="$DESTINO/copiar_no_duplicados_${TS}.log"

echo "📂 Carpeta origen: $ORIGEN"
echo "📁 Carpeta destino: $DESTINO"
echo "🧾 Archivo de duplicados: $DUP_FILE"
echo "📝 Log: $LOG"
echo

# ------------------------------------------------------------
# 1️⃣ Construir lista de rutas a excluir
# ------------------------------------------------------------
EXCL_TMP=$(mktemp)
grep -v '^#' "$DUP_FILE" | grep -E '^/' > "$EXCL_TMP" || true

# ------------------------------------------------------------
# 2️⃣ Encontrar archivos válidos (no duplicados ni excluidos)
# ------------------------------------------------------------
echo "🔎 Escaneando archivos válidos..."

find "$ORIGEN" \
  \( -type d \( \
      -name ".*" -o -name "\$*" \
      -o -iname "Archivos de programa" -o -iname "Descargas" -o -iname "Downloads" \
      -o -iname "Documents and Settings" -o -iname "Logs" -o -iname "PerfLogs" \
      -o -iname "Program Files" -o -iname "Program Files (x86)" -o -iname "ProgramData" \
      -o -iname "Recovery" -o -iname "System Volume Information" -o -iname "Temp" \
      -o -iname "Users" -o -iname "Windows" -o -iname "Windows10Upgrade" \
    \) -prune \) \
  -o -type f \
     ! -iname "*.exe" ! -iname "*.log" ! -iname "*.dll" ! -iname "*.sys" \
     ! -iname "*.part" ! -iname "*.crdownload" ! -iname "*.tmp" \
     ! -iname "*.partial" ! -iname "*.download" ! -iname "*!qB*" ! -iname "*.aria2" \
  -print0 2>/dev/null \
| grep -zavFf "$EXCL_TMP" \
| tee >(xargs -0 -I{} bash -c '
      FILE="{}"
      REL="${FILE#'"$ORIGEN"'/}"
      DEST_FILE="'"$DESTINO"'/$REL"
      mkdir -p "$(dirname "$DEST_FILE")"
      cp -p "$FILE" "$DEST_FILE"
      echo "✔ Copiado: $REL" >> "'"$LOG"'"
  ') > /dev/null

# ------------------------------------------------------------
# 3️⃣ Mostrar resumen
# ------------------------------------------------------------
COPIADOS=$(grep -c "✔ Copiado:" "$LOG" || echo 0)
echo
echo "✅ Copia completada."
echo "📦 Archivos copiados: $COPIADOS"
echo "📄 Log detallado: $LOG"
echo




sábado, 11 de octubre de 2025

WEBPROPv2 (XII). Mostrar ficheros en un control FastHTML

 Veamos el código que ejecuta en python  un uvicorn y muestra el fichero que le decimos:

El truco está en utilizar libreoffice para transformar los ficheros de ofimática ( ods,odt, xls,xlsx, doc,docx ..) a html y mostrar el html generado.

Si el fichero es grande puede enlentecer mucho la carga, conversión y muestra

import base64
import mimetypes
from fasthtml import common as fh
import uvicorn
from pathlib import Path
import subprocess
import shutil


app = fh.FastHTML()



def convertir_a_html(path: Path) -> Path | None:
    """Convierte el archivo a HTML usando LibreOffice y devuelve la ruta resultante."""
    output_dir = Path("/tmp")
    result = subprocess.run(
        ["libreoffice", "--headless", "--convert-to", "html", str(path), "--outdir", str(output_dir)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    if result.returncode == 0:
        html_file = output_dir / (path.stem + ".html")
        if html_file.exists():
            return html_file
    return None

def generar_viewer(path: Path):
    mime_type, _ = mimetypes.guess_type(str(path))
    if not mime_type:
        mime_type = "application/octet-stream"

    data = path.read_bytes()
    b64 = base64.b64encode(data).decode("utf-8")

    # Imágenes
    if mime_type.startswith("image/"):
        return fh.Img(src=f"data:{mime_type};base64,{b64}", cls="img-fluid rounded shadow", alt=path.name)

    # PDF
    if mime_type == "application/pdf":
        return fh.Iframe(src=f"data:{mime_type};base64,{b64}",
                         style="width:100%; height:90vh;",
                         cls="border rounded shadow-sm",
                         title=path.name)

    # Texto
    if mime_type.startswith("text/"):
        content = data.decode("utf-8", errors="ignore")
        return fh.Pre(content, cls="bg-light p-3 border rounded")

    # ODS / XLSX / DOCX — convertir a HTML
    convertible_types = {
        "application/vnd.oasis.opendocument.spreadsheet",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.oasis.opendocument.text",
        'application/msword',
        'application/vnd.ms-excel',
    }
    if mime_type in convertible_types:
        html_file = convertir_a_html(path)
        if html_file:
            html_content = html_file.read_text(encoding="utf-8", errors="ignore")
            return fh.Div(fh.NotStr(html_content), cls="border p-2 bg-white shadow-sm")

    # Otros: ofrecer descarga
    return fh.Div(
        fh.P("El archivo no puede mostrarse directamente en el navegador.", cls="text-muted"),
        fh.A("Descargar archivo", href=f"data:{mime_type};base64,{b64}",
             download=path.name, cls="btn btn-primary mt-3"),
        cls="text-center"
    )

@app.get("/mostrarfichero")
async def mostrar_fichero(req):
    path_param = req.query_params.get("pathfichero")
    if not path_param:
        return fh.Html(fh.Body(fh.H4("⚠️ Falta el parámetro 'pathfichero'")))
    path = Path(path_param)
    if not path.exists():
        return fh.Html(fh.Body(fh.H4(f"❌ Archivo no encontrado: {path_param}")))
    viewer = generar_viewer(path)
    return fh.Html(
        fh.Head(fh.Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css")),
        fh.Body(
            fh.Div(
                fh.H3(f"Mostrando: {path.name}", cls="text-center my-4"),
                viewer,
                cls="container"
            )
        )
    )

if __name__ == "__main__":
    print("🚀 Abre en el navegador: http://localhost:8000/mostrarfichero?pathfichero=/home/edu/kk/ODS01.ods")
    uvicorn.run(app, host="0.0.0.0", port=8000)


Y para ejecutarlo, tal como se dice en as últimas sentencias, ejecutamos el programa python desde visual studio code y en un navegador apuntamos a esa ruta donde en /home/kk/OSD01.ods está un fichero de hoja de cálculo

http://localhost:8000/mostrarfichero?pathfichero=/home/edu/kk/ODS01.ods


martes, 7 de octubre de 2025

WEBPROPv2 (XI). Arranque de los servicios de la aplicacion y validación de usuarios

 Tal como vimos en el apartado anterior de solución de errores, es mejor arrancar estas aplicaciones como servicios, así se pueden reactivar sin problemas.

También se vió como se debe arrancar uvicorn para que pueda tener varios workers en marcha y así no bloquee su ejecución a otros usuarios si está ejecutando una tarea larga como la de captura de decretos.

Veamos los ficheros de servicio que se guardan en /etc/systemd/system

python_menus_mnu_main_py.service:

[Unit]
Description=Softprop Menu Main Python Service
After=network.target

[Service]
# Usuario y grupo que ejecutarán el servicio
User=informatica
Group=informatica

# Directorio de trabajo base
WorkingDirectory=/home/informatica/eduApps

# Activar entorno virtual
Environment="PATH=/home/informatica/eduApps/softprop/venv_softprop/bin"

# Ejecutar directamente el script (usa el shebang) SOLO USA UN WORKER
#ExecStart=/home/informatica/eduApps/softprop/menus/mnu_main.py
# Ejecutar uvicorn directamente (importa el módulo mnu_main:app)
ExecStart=/home/informatica/eduApps/softprop/venv_softprop/bin/uvicorn softprop.menus.mnu_main:app \
  --host proves.tavernes.es \
  --port 5000 \
  --workers 4 \
  --ssl-keyfile /home/informatica/eduApps/softprop/static/certs/wildcard.municipio.es.key \
  --ssl-certfile /home/informatica/eduApps/softprop/static/certs/wildcard.municipio.es.crt

# Reinicio automático en caso de error
Restart=always
RestartSec=5

# Redirección de logs
StandardOutput=append:/var/log/softprop_menus_menu_main.log
StandardError=append:/var/log/softprop_menus_mnu_main.err

[Install]
WantedBy=multi-user.target


y para el servicio de autenticación python_authentication_xmopenresty_py.service:



[Unit]
Description=Softprop OpenResty Python Service After=network.target
[Service]
# Usuario y grupo del servicio User=informatica Group=informatica # Directorio base del proyecto WorkingDirectory=/home/informatica/eduApps # Activar entorno virtual Environment="PATH=/home/informatica/eduApps/softprop/venv_softprop/bin" #ExecStart=/home/informatica/eduApps/softprop/authentication/xmopenresty.py # Ejecutar uvicorn directamente importando el módulo ExecStart=/home/informatica/eduApps/softprop/venv_softprop/bin/uvicorn softprop.authentication.xmopenresty:app \ --host 192.168.28.16 \ --port 5001 \ --workers 4 \ --ssl-keyfile /home/informatica/eduApps/softprop/static/certs/wildcard.municipio.es.key \ --ssl-certfile /home/informatica/eduApps/softprop/static/certs/wildcard.municipio.es.crt # Reiniciar automáticamente en caso de error Restart=always RestartSec=5 #Logs StandardOutput=append:/var/log/softprop_authentication_openresty.log StandardError=append:/var/log/softprop_authentication_openresty.err
[Install]
WantedBy=multi-user.target

Ahora hay que hacer estas tareas:

sudo systemctl restart python_menus_mnu_main_py.service
sudo systemctl restart python_authentication_xmopenresty_py.service

y sobre todo controlar los LOGS !! que crecen mucho

/var/log/softprop_menus_menu_main.log
/var/log/softprop_menus_mnu_main.err
/var/log/softprop_authentication_openresty.log
/var/log/softprop_authentication_openresty.err








sábado, 4 de octubre de 2025

Disco NTFS con problemas de lectura.Multiples ficheros duplicados.Copiar sin duplicados

 1. Detectar el problema

En la herramienta "disks" de Ubuntu me dice:

s'ha produit un error en muntar el sistema de fitxers Error mounting /dev/sdc4 at /media/eduard/22143F41143F176F;wrong fs type, bad option, bad superblock on /dev/sdc4, missing codepage or helper program, or other error (udisks-error-quark,0)

Por tanto el problema es en el disco /dev/sdc4


2. Detectar el tipo de partición

Ejecutamos:

sudo fdisk -l

Y vemos que es NTFS (que también se podía ver en la herramienta "Disks" de ubuntu

3. Probar a montar manualmente

Ejecutamos:

sudo mount -t ntfs /dev/sdc4 /mnt

y responde:

$MFTMirr does not match $MFT (record 3). Failed to mount '/dev/sdc4': Error d’Entrada/Sortida NTFS is either inconsistent, or there is a hardware fault, or it's a SoftRAID/FakeRAID hardware. In the first case run chkdsk /f on Windows then reboot into Windows twice. The usage of the /f parameter is very important! If the device is a SoftRAID/FakeRAID then first activate it and mount a different device under the /dev/mapper/ directory, (e.g. /dev/mapper/nvidia_eahaabcc1). Please see the 'dmraid' documentation for more details.

4. Instalar software adicional y utilizarlo


sudo apt install ntfs-3g

sudo ntfsfix /dev/sdc4

y contesta:
Mounting volume... Windows is hibernated, refused to mount. FAILED Attempting to correct errors... Processing $MFT and $MFTMirr... Reading $MFT... OK Reading $MFTMirr... OK Comparing $MFTMirr to $MFT... OK Processing of $MFT and $MFTMirr completed successfully. Setting required flags on partition... OK Going to empty the journal ($LogFile)... OK Windows is hibernated, refused to mount. Remount failed: Operation not permitted

cosa que dice que windows está "hibernated" y se debe a que se interrumpioun proceso de E/AS en el disco y que no se debe acceder a el para modificar nada si no se opera desde windows.

Por tanto lo que podemos hacer es montarlo de solo lectura y copiar los datos 

4. Montar el disco de solo lectura


sudo mount -t ntfs-3g -o ro /dev/sdc4 /mnt

y ya podemos acceder a el disco o la partición en el directorio /mnt

5. Copia de seguridad multifichero


Se opta por copiar a otro disco mas grande y luego crear un zip multivolumen de ficheros de 4 gigas con:

cd /home/eduard
zip -r -s 4g MIS_DATOS.zip MIS_DATOS

donde la carpeta que contiene los datos a guradar es /home/eduard/MIS_DATOS
 y esto genera estos ficheros:

MIS_DATOS.z01, MIS_DATOS.z02, ....MIS_DATOS.88, MIS_DATOS.zip

Siendo MIS_DATOS.88 en mi caso el penúltimo fichero y MIS_DATOS.zip el último

Para descomprimir utilizar:

unzip MIS_DATOS.zip

asegurándose que todos los ficheros generados esten disponibles en la misma carpeta.

Si solo se quiere extraer una subcarpeta llamada MI_SUBCARPETA se haría_

unzip MIS_DATOS.zip "MIS_DATOS/MI_SUBCARPETA/*" -d /ruta/de/destino

y si solo queremos listar el contenido:

unzip -l MIS_DATOS.zip | less


6. Ver si hay muchos duplicados. Borrado y copia sin duplicados


Con chatgpt he creado un bash para detectar duplicados y en su caso borrarlos. Pero en el siguiente punto se ha hecho un bash que solo copia los ficheros no duplicados.

El fichero a no borrar de los duplicados es aquel que tiene menor numero de anidamientos en carpeta.

Ver y/o eliminar duplicados (buscar_duplicados_carpeta.sh)

#!/bin/bash
# buscar_duplicados.sh
# Busca y (opcionalmente) elimina ficheros duplicados (por nombre y tamaño)
# e informa del espacio total y del potencial ahorro.

# Uso:
#   ./buscar_duplicados.sh /ruta/a/carpeta [eliminar_duplicados]

BASE_DIR="$1"
ACCION="$2"

# --- Validaciones iniciales
if [[ -z "$BASE_DIR" ]]; then
  echo "❌ Uso: $0 /ruta/a/carpeta [eliminar_duplicados]"
  exit 1
fi

if [[ ! -d "$BASE_DIR" ]]; then
  echo "❌ No existe el directorio: $BASE_DIR"
  exit 1
fi

# --- Variables
REPORT_FILE="duplicados_$(date +%Y%m%d_%H%M%S).txt"
> "$REPORT_FILE"
ELIMINAR=false
if [[ "$ACCION" == "eliminar_duplicados" ]]; then
  ELIMINAR=true
fi

echo "🔍 Buscando duplicados en: $BASE_DIR"
echo "📝 Informe: $REPORT_FILE"
echo

# --- Calcular espacio total de la carpeta (en bytes)
TOTAL_BYTES=$(du -sb "$BASE_DIR" | awk '{print $1}')
TOTAL_HUMAN=$(du -sh "$BASE_DIR" | awk '{print $1}')

echo "💾 Espacio total actual ocupado por la carpeta: $TOTAL_HUMAN ($TOTAL_BYTES bytes)"
echo "💾 Espacio total actual ocupado por la carpeta: $TOTAL_HUMAN ($TOTAL_BYTES bytes)" >> "$REPORT_FILE"
echo

# --- Buscar duplicados
POTENCIAL_AHORRO=0

find "$BASE_DIR" -type f -printf '%s\t%p\n' | \
while IFS=$'\t' read -r SIZE FILE; do
    BASENAME=$(basename "$FILE")
    echo -e "$BASENAME\t$SIZE\t$FILE"
done | sort -k1,1 -k2,2n | \
awk -F'\t' -v ELIMINAR="$ELIMINAR" -v REPORT_FILE="$REPORT_FILE" -v BASE_DIR="$BASE_DIR" '
function human(bytes) {
    hum[1024^4]="TB"; hum[1024^3]="GB"; hum[1024^2]="MB"; hum[1024]="KB";
    for (x=1024^4; x>=1024; x/=1024) if (bytes>=x) return sprintf("%.2f %s", bytes/x, hum[x]);
    return bytes " B";
}

{
    name=$1; size=$2; path=$3;
    key=name"|"size;
    files[key]=files[key]?files[key]"\n"path:path;
    sizes[key]=size;
}
END {
    total_savings=0;
    for (k in files) {
        split(files[k], arr, "\n");
        if (length(arr) > 1) {
            print "📁 Duplicado: " k >> REPORT_FILE;
            print "📁 Duplicado: " k;

            # Calcular profundidad
            min=999999; minpath="";
            for (i in arr) {
                split(arr[i], dirs, "/");
                depth[i]=length(dirs);
                if (depth[i] < min) {
                    min=depth[i];
                    minpath=arr[i];
                }
            }

            print "   ✅ Se conservará: " minpath >> REPORT_FILE;
            print "   ✅ Se conservará: " minpath;

            print "   ❌ Duplicados encontrados:" >> REPORT_FILE;
            for (i in arr) {
                if (arr[i] != minpath && arr[i] != "") {
                    print "      " arr[i] >> REPORT_FILE;
                    print "      " arr[i];
                    total_savings += sizes[k];  # sumamos el tamaño de los que se podrían eliminar
                }
            }

            if (ELIMINAR == "true") {
                printf "\n¿Eliminar todos los duplicados de este grupo (excepto el conservado)? (s/n): ";
                getline resp < "/dev/tty";
                if (resp == "s" || resp == "S") {
                    for (i in arr) {
                        if (arr[i] != minpath && arr[i] != "") {
                            cmd="rm -i \""arr[i]"\"";
                            system(cmd);
                            print "      ➜ Eliminado: " arr[i] >> REPORT_FILE;
                        }
                    }
                } else {
                    print "      ➜ No se eliminaron duplicados de este grupo." >> REPORT_FILE;
                }
            }
            print "" >> REPORT_FILE;
            print "";
        }
    }
    print "--------------------------------------------" >> REPORT_FILE;
    print "📊 Espacio total potencial a liberar: " human(total_savings) " (" total_savings " bytes)" >> REPORT_FILE;
    print "--------------------------------------------" >> REPORT_FILE;

    print "\n📊 Espacio total potencial a liberar: " human(total_savings) " (" total_savings " bytes)";
}' 

echo
echo "✅ Proceso finalizado."
echo "📄 Informe generado en: $(realpath "$REPORT_FILE")"

# --- Calcular de nuevo el tamaño si se eliminaron duplicados
if [[ "$ELIMINAR" == "true" ]]; then
  FINAL_BYTES=$(du -sb "$BASE_DIR" | awk '{print $1}')
  FINAL_HUMAN=$(du -sh "$BASE_DIR" | awk '{print $1}')
  echo
  echo "💾 Espacio tras eliminar duplicados: $FINAL_HUMAN ($FINAL_BYTES bytes)"
  echo "💾 Espacio tras eliminar duplicados: $FINAL_HUMAN ($FINAL_BYTES bytes)" >> "$REPORT_FILE"
fi

copiar carpeta sin duplicados creando un fichero de duplicados dentro de la carpeta destino (copiar_carpeta_no_duplicados.sh)


#!/bin/bash
# copiar_sin_duplicados.sh
# Copia solo los ficheros no duplicados (por nombre y tamaño)
# y genera informes de los archivos copiados y duplicados.

echo "📁 Introduce la ruta de la carpeta origen:"
read -r ORIGEN

if [[ ! -d "$ORIGEN" ]]; then
  echo "❌ La carpeta origen no existe."
  exit 1
fi

echo "📂 Introduce la ruta de la carpeta destino:"
read -r DESTINO

# Crear carpeta destino si no existe
mkdir -p "$DESTINO"

# Archivos temporales e informes
TMP_FILE=$(mktemp)
REPORT_FILE="copiados_$(date +%Y%m%d_%H%M%S).txt"
DUPLICADOS_FILE="$DESTINO/duplicados_$(date +%Y%m%d_%H%M%S).txt"
> "$REPORT_FILE"
> "$DUPLICADOS_FILE"

echo "🔍 Analizando duplicados en: $ORIGEN"
echo

# --- Calcular tamaño total de la carpeta origen
TOTAL_BYTES=$(du -sb "$ORIGEN" | awk '{print $1}')
TOTAL_HUMAN=$(du -sh "$ORIGEN" | awk '{print $1}')
echo "💾 Tamaño total de la carpeta origen: $TOTAL_HUMAN ($TOTAL_BYTES bytes)"
echo "💾 Tamaño total de la carpeta origen: $TOTAL_HUMAN ($TOTAL_BYTES bytes)" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

# --- Buscar duplicados (por nombre + tamaño)
find "$ORIGEN" -type f -printf '%s\t%p\n' | \
while IFS=$'\t' read -r SIZE FILE; do
    BASENAME=$(basename "$FILE")
    echo -e "$BASENAME\t$SIZE\t$FILE"
done | sort -k1,1 -k2,2n | \
awk -F'\t' -v TMP_FILE="$TMP_FILE" -v DUPLICADOS_FILE="$DUPLICADOS_FILE" '
{
    name=$1; size=$2; path=$3;
    key=name"|"size;
    files[key]=files[key]?files[key]"\n"path:path;
    sizes[key]=size;
}
END {
    for (k in files) {
        split(files[k], arr, "\n");
        if (length(arr) > 1) {
            print "-----------------------------------" >> DUPLICADOS_FILE;
            print "Duplicado (" k "):" >> DUPLICADOS_FILE;
            for (i in arr) {
                print arr[i] >> TMP_FILE;
                print "   " arr[i] >> DUPLICADOS_FILE;
            }
        }
    }
}'

# --- Copiar solo los no duplicados
echo "🚀 Iniciando copia de ficheros no duplicados..."
echo >> "$REPORT_FILE"
echo "=============================" >> "$REPORT_FILE"
echo "COPIA DE FICHEROS NO DUPLICADOS" >> "$REPORT_FILE"
echo "=============================" >> "$REPORT_FILE"
echo >> "$REPORT_FILE"

COUNT=0
BYTES_TOTAL_COPIADOS=0
BYTES_DUPLICADOS=0

# Calcular espacio potencial de duplicados
while IFS= read -r FILE_DUP; do
    if [[ -f "$FILE_DUP" ]]; then
        SIZE=$(stat -c%s "$FILE_DUP")
        BYTES_DUPLICADOS=$((BYTES_DUPLICADOS + SIZE))
    fi
done < "$TMP_FILE"

# Copiar los no duplicados
while IFS= read -r FILE; do
    # Saltar duplicados
    if grep -Fxq "$FILE" "$TMP_FILE"; then
        continue
    fi

    REL_PATH="${FILE#$ORIGEN/}"
    DEST_PATH="$DESTINO/$REL_PATH"
    DEST_DIR=$(dirname "$DEST_PATH")

    mkdir -p "$DEST_DIR"
    cp -p "$FILE" "$DEST_PATH"

    SIZE=$(stat -c%s "$FILE")
    BYTES_TOTAL_COPIADOS=$((BYTES_TOTAL_COPIADOS + SIZE))
    COUNT=$((COUNT + 1))

    echo "✔ Copiado: $DEST_PATH" >> "$REPORT_FILE"
done < <(find "$ORIGEN" -type f)

# --- Función para tamaños legibles
function human_readable {
    local bytes=$1
    local units=("B" "KB" "MB" "GB" "TB")
    local i=0
    local value=$bytes
    while (( value >= 1024 && i < 4 )); do
        value=$(( value / 1024 ))
        ((i++))
    done
    echo "${value} ${units[$i]}"
}

TOTAL_HUMAN_COPIADOS=$(human_readable "$BYTES_TOTAL_COPIADOS")
TOTAL_HUMAN_DUPLICADOS=$(human_readable "$BYTES_DUPLICADOS")

# --- Resultados finales
echo
echo "✅ Copia completada."
echo "📊 Archivos copiados: $COUNT"
echo "📦 Tamaño total origen: $TOTAL_HUMAN ($TOTAL_BYTES bytes)"
echo "📥 Tamaño copiado (sin duplicados): $TOTAL_HUMAN_COPIADOS ($BYTES_TOTAL_COPIADOS bytes)"
echo "💾 Espacio ahorrado (duplicados no copiados): $TOTAL_HUMAN_DUPLICADOS ($BYTES_DUPLICADOS bytes)"
echo
echo "📄 Informe de copiados: $(realpath "$REPORT_FILE")"
echo "📁 Listado de duplicados: $(realpath "$DUPLICADOS_FILE")"

echo >> "$REPORT_FILE"
echo "-------------------------------------" >> "$REPORT_FILE"
echo "📦 Tamaño total origen: $TOTAL_HUMAN ($TOTAL_BYTES bytes)" >> "$REPORT_FILE"
echo "📥 Tamaño copiado (sin duplicados): $TOTAL_HUMAN_COPIADOS ($BYTES_TOTAL_COPIADOS bytes)" >> "$REPORT_FILE"
echo "💾 Espacio ahorrado (duplicados no copiados): $TOTAL_HUMAN_DUPLICADOS ($BYTES_DUPLICADOS bytes)" >> "$REPORT_FILE"
echo "-------------------------------------" >> "$REPORT_FILE"

# --- Limpieza
rm -f "$TMP_FILE"