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
- Necesitas duplicar archivos de configuración durante despliegues.
- Estás moviendo archivos subidos desde directorios temporales a almacenamiento permanente y quieres validarlos primero (consulta la validación de archivos subidos).
- Quieres rotar o archivar archivos de log automáticamente.
- Estás copiando archivos en lote antes de comprimirlos.
Cuándo Evitar
- Tu caso de uso es replicación continua (no copias de una sola vez), donde
rsync --daemonolsyncdestá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
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ía | Enfoque | Mejor para |
|---|---|---|
| Python | shutil + pathlib | Scripts multiplataforma y pipelines de datos |
| JavaScript | fs.promises / fs.cp | Tooling de Node.js y scripts de CI |
| Java | java.nio.file.Files | Servicios de producción que necesitan opciones tipadas |
| Bash | cp, mv, sha256sum | Tareas 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_EXCLo 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
movees atómico cuando origen y destino están en distintos filesystems o particiones. - Construir rutas a mano concatenando strings (
"/" + folder + "/" + name) en lugar de usarpathlib,path.joinoPath.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
- Python shutil: copiar, mover y árboles.
- Node.js fs.promises:
copyFile,rename,cpcon opciones. - Java NIO Files
- Bash cp y mv: metadatos y sobrescritura.
- rsync man page: sync con checksums y reanudación.
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.
Recursos Relacionados
Observar Cambios en Archivos
Cómo monitorear cambios en el sistema de archivos en tiempo real.
RecipeLeer Archivos Grandes
Cómo leer archivos grandes de forma eficiente sin agotar la memoria.
RecipeEscribir Archivos Grandes
Cómo escribir archivos grandes de forma eficiente usando salida bufferizada y streaming.
RecipeValidación de Subida de Archivos
Cómo manejar subidas de archivos de forma segura con validación de tamaño, tipo y contenido.
RecipeComprimir y Descomprimir Archivos
Cómo manejar archivos ZIP, GZIP y TAR programáticamente.
RecipeRotar Archivos de Log
Cómo implementar rotación de logs por tamaño, fecha y cantidad para prevenir la exaustión de disco en Python, Node.js, Java y Linux.