StackPractices
beginner Por Mathias Paulenko

Cómo Copiar y Mover Archivos con Python, JS, Java y Bash

Aprende a copiar y mover archivos multiplataforma con Python, JavaScript, Java y Bash. Incluye movimientos atómicos, checksums, symlinks y patrones batch.

Visión General

Copiar y mover archivos parece simple hasta que una transferencia parcial, un error de permisos o un renombre entre dispositivos corrompe los datos. Esta receta trae patrones prácticos en Python, JavaScript, Java y Bash que cubren sobrescrituras, checksums, symlinks y movimientos atómicos.

Cuándo Usar

Cuándo Evitar

  • Tu caso de uso es replicación continua (no copias de una sola vez), donde rsync --daemon o lsyncd están diseñados específicamente para mantener directorios sincronizados a lo largo del tiempo.
  • Estás moviendo objetos grandes a la nube. El SDK o CLI del proveedor maneja mejor las cargas multipartes.
  • Los usuarios finales necesitan un gestor gráfico de archivos; esto es una receta de scripting.

Solución

Python

import shutil
from pathlib import Path
import hashlib

def safe_copy(src, dest, *, overwrite=False, verify=True, follow_symlinks=False):
    """Copia un archivo, preservando metadatos y verificando integridad opcionalmente."""
    src, dest = Path(src), Path(dest)
    if not src.exists():
        raise FileNotFoundError(f"Origen no encontrado: {src}")
    if dest.exists() and not overwrite:
        raise FileExistsError(f"Destino existe: {dest}")
    dest.parent.mkdir(parents=True, exist_ok=True)

    if src.is_symlink() and not follow_symlinks:
        dest.symlink_to(Path(src).readlink())
    else:
        shutil.copy2(src, dest)

    if verify and not src.is_symlink():
        src_hash = hashlib.sha256(src.read_bytes()).hexdigest()
        dest_hash = hashlib.sha256(dest.read_bytes()).hexdigest()
        if src_hash != dest_hash:
            dest.unlink()
            raise IOError(f"Checksum no coincide: {src} -> {dest}")
    return dest

def safe_move(src, dest, *, overwrite=False):
    """Mueve un archivo, usando copiar+borrar si está entre filesystems."""
    src, dest = Path(src), Path(dest)
    if not src.exists():
        raise FileNotFoundError(f"Origen no encontrado: {src}")
    if dest.exists() and not overwrite:
        raise FileExistsError(f"Destino existe: {dest}")
    dest.parent.mkdir(parents=True, exist_ok=True)

    try:
        shutil.move(str(src), str(dest))
    except shutil.Error:
        safe_copy(src, dest, overwrite=overwrite, verify=True)
        src.unlink()
    return dest

def batch_copy(src_dir, dest_dir, pattern="*", *, overwrite=False):
    """Copia todos los archivos que coinciden con un patrón de src_dir a dest_dir."""
    src_dir, dest_dir = Path(src_dir), Path(dest_dir)
    dest_dir.mkdir(parents=True, exist_ok=True)
    copied = []
    for file in src_dir.glob(pattern):
        if file.is_file():
            copied.append(safe_copy(file, dest_dir / file.name, overwrite=overwrite))
    return copied

JavaScript

const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');

async function sha256(file) {
  const data = await fs.readFile(file);
  return crypto.createHash('sha256').update(data).digest('hex');
}

async function copyWithChecksum(src, dest) {
  // COPYFILE_FICLONE intenta un clon copy-on-write cuando se soporta
  await fs.copyFile(src, dest, fs.constants.COPYFILE_FICLONE);
  if (await sha256(src) !== await sha256(dest)) {
    await fs.unlink(dest);
    throw new Error(`Checksum no coincide: ${src} -> ${dest}`);
  }
}

async function moveWithFallback(src, dest) {
  try {
    await fs.rename(src, dest);  // atómico si es el mismo filesystem
  } catch (err) {
    if (err.code === 'EXDEV') {
      const stat = await fs.stat(src);
      if (stat.isDirectory()) {
        await fs.cp(src, dest, { recursive: true });  // Node 16.7+
        await fs.rm(src, { recursive: true });
      } else {
        await copyWithChecksum(src, dest);
        await fs.unlink(src);
      }
    } else {
      throw err;
    }
  }
}

Java

import java.nio.file.*;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;

public class FileCopier {

    public static void copyWithAttributes(Path src, Path dest, boolean overwrite) throws Exception {
        List<CopyOption> options = new ArrayList<>();
        options.add(StandardCopyOption.COPY_ATTRIBUTES);
        if (overwrite) options.add(StandardCopyOption.REPLACE_EXISTING);
        Files.copy(src, dest, options.toArray(new CopyOption[0]));
    }

    public static void moveWithFallback(Path src, Path dest, boolean overwrite) throws Exception {
        List<CopyOption> options = new ArrayList<>();
        if (overwrite) options.add(StandardCopyOption.REPLACE_EXISTING);

        try {
            // ATOMIC_MOVE solo funciona dentro del mismo filesystem
            options.add(StandardCopyOption.ATOMIC_MOVE);
            Files.move(src, dest, options.toArray(new CopyOption[0]));
        } catch (AtomicMoveNotSupportedException e) {
            options.remove(StandardCopyOption.ATOMIC_MOVE);
            copyWithAttributes(src, dest, overwrite);
            Files.deleteIfExists(src);
        }
    }

    public static String sha256(Path file) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] hash = md.digest(Files.readAllBytes(file));
        StringBuilder sb = new StringBuilder();
        for (byte b : hash) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}

Bash

#!/usr/bin/env bash
set -euo pipefail

safe_copy() {
    local src="$1"
    local dest="$2"
    local overwrite="${3:-false}"

    [[ -f "$src" ]] || { echo "ERROR: Origen no encontrado: $src"; return 1; }

    if [[ -f "$dest" && "$overwrite" != "true" ]]; then
        echo "ERROR: Destino existe: $dest"
        return 1
    fi

    mkdir -p "$(dirname "$dest")"
    cp -p "$src" "$dest"

    local src_sum dest_sum
    src_sum=$(sha256sum "$src" | cut -d' ' -f1)
    dest_sum=$(sha256sum "$dest" | cut -d' ' -f1)

    if [[ "$src_sum" != "$dest_sum" ]]; then
        rm -f "$dest"
        echo "ERROR: Checksum no coincide después de copiar"
        return 1
    fi

    echo "OK: $src -> $dest (verificado)"
}

batch_copy() {
    local src_dir="$1"
    local dest_dir="$2"
    local pattern="${3:-*}"

    mkdir -p "$dest_dir"
    local count=0
    for file in "$src_dir"/$pattern; do
        [[ -f "$file" ]] || continue
        safe_copy "$file" "$dest_dir/$(basename "$file")" true && count=$((count + 1))
    done
    echo "Copiados $count archivos"
}

Explicación

flowchart diagram: Archivo Origen

Copiar duplica contenido y, opcionalmente, metadatos. Mover dentro del mismo filesystem es un renombre rápido y atómico del inode. Los movimientos entre dispositivos tienen que copiar los bytes primero y luego borrar el origen; si algo falla a mitad de camino, puedes terminar con un archivo parcial o un duplicado.

El flag ATOMIC_MOVE de Java y fs.rename de Node solo garantizan atomicidad cuando origen y destino están en el mismo filesystem. Para escrituras críticas, un patrón común es escribir a un archivo temporal en el directorio destino y luego renombrarlo sobre el objetivo.

Variantes

TecnologíaEnfoqueMejor para
Pythonshutil + pathlibScripts multiplataforma y pipelines de datos
JavaScriptfs.promises / fs.cpTooling de Node.js y scripts de CI
Javajava.nio.file.FilesServicios de producción que necesitan opciones tipadas
Bashcp, mv, sha256sumTareas rápidas de sysadmin y cron jobs

Otras variantes útiles incluyen sendfile/copy_file_range para copias a nivel de kernel en Linux, fs-extra para copias recursivas con filtros en Node.js, y Apache Commons IO FileUtils para helpers batch de más alto nivel en Java.

Mejores Prácticas

  • Verifica siempre las sobrescrituras con COPYFILE_EXCL o un flag explícito, para no reemplazar datos sin darte cuenta.
  • Para archivos críticos, escribe en un archivo temporal del mismo directorio y luego muévelo al nombre final.
  • Verifica checksums en archivos grandes, copias por red o cualquier operación donde perder datos te costaría caro.
  • Crea los directorios padre antes de escribir, o la operación fallará con “No such file or directory”.
  • Decide la política de symlinks desde el inicio: síguelos para backups, copia el enlace para preservar la estructura.
  • Maneja movimientos EXDEV/entre dispositivos con un fallback de copiar y borrar.

Errores Comunes

  • Sobrescribir archivos existentes antes de confirmar o sin backup.
  • Asumir que move es atómico cuando origen y destino están en distintos filesystems o particiones.
  • Construir rutas a mano concatenando strings ("/" + folder + "/" + name) en lugar de usar pathlib, path.join o Path.resolve. Esto rompe en Windows por los separadores con barra invertida.
  • Ignorar symlinks, de modo que un backup capture el enlace en lugar del contenido, o el contenido en lugar del enlace.
  • Mover archivos que otro proceso aún sigue escribiendo.
  • Olvidarte de crear el directorio destino y recibir un error críptico de “No such file or directory”.

Resumen

Un move dentro del mismo filesystem es un renombre atómico rápido; un move entre dispositivos es copiar-y-borrar y puede fallar a mitad de camino. Siempre usá SHA-256 en copias grandes o críticas, y si el hash no coincide, borra el destino y reintenta. Maneja EXDEV (Node) y AtomicMoveNotSupportedException (Java) con un fallback de copiar y borrar. Para actualizaciones todo-o-nada, escribí un archivo temporal en el directorio destino y renómbralo sobre el objetivo para que los lectores nunca vean un archivo parcial. Decidí la política de symlinks desde el inicio: seguilos para backups, copia el enlace para preservar la estructura. Creá los directorios padre antes de escribir, o la operación fallará con un error críptico de “No such file or directory”. Para archivos grandes, las copias a nivel de kernel (sendfile, copy_file_range, COPYFILE_FICLONE) superan a los bucles por chunks.

See Also

Preguntas frecuentes

¿Es move siempre atómico?

Un move solo es atómico dentro del mismo filesystem, y los movimientos entre dispositivos son operaciones de copiar y borrar que pueden interrumpirse. Cuando los lectores necesiten una actualización todo-o-nada, escribí un archivo temporal y renómbralo en su lugar.

¿Cómo copio directorios recursivamente?

En Python, usa shutil.copytree(). En JavaScript, usa fs.cp(src, dest, { recursive: true }) (Node 16.7+) o fs-extra.copy(). En Java, usa Files.walkFileTree() o Apache Commons IO FileUtils.copyDirectory(). En Bash, cp -r sirve para copias rápidas; rsync -a preserva permisos y puede reanudar transferencias interrumpidas.

¿Debo seguir symlinks al copiar?

La respuesta corta: depende de si estás haciendo un backup o preservando estructura. Para backups, sigue los symlinks y copia el contenido real en lugar de una referencia colgada. Para preservar la estructura exacta del directorio, copia el symlink mismo. Python, Java y Node exponen flags para decidir.

¿Qué hago si falla un checksum?

Borra la copia de destino, revisa que el origen no esté corrupto y reintenta. Si el hash no coincide, el destino está corrupto y conservarlo solo desperdicia disco, así que empezá de nuevo.

¿Cómo manejo errores de permisos?

Los errores de permisos son la falla de copia más común que veo en producción. La solución es aburrida pero confiable: antes de empezar, verifica que el origen sea legible y el directorio destino escribible. En Python, captura PermissionError; en Node, revisa err.code === 'EACCES'; en Java, captura AccessDeniedException. Si estás copiando a un directorio compartido, asegúrate de que el usuario destino sea dueño de la ruta o tenga permiso de escritura en grupo. No ejecutes como root, porque oculta problemas de permisos que van a salir a la luz después en producción, así que corré el script como el usuario del servicio.

¿Cuál es la forma más rápida de copiar archivos grandes?

Una vez que pasás los 1 GB, el bucle de checksum se vuelve el cuello de botella, no la copia en sí. Saltealo y apoyate en llamadas a nivel de kernel. En Linux, sendfile o copy_file_range evitan leer datos a userspace. Node con COPYFILE_FICLONE intenta copy-on-write en btrfs o XFS reflink, que no cuesta nada. shutil.copy2 de Python cae en un bucle de lectura/escritura por chunks, que es más lento. Para lotes de archivos grandes, rsync -a --checksum es difícil de superar porque saltea los archivos que ya coinciden.