Subir archivos
La subida de archivos es una funcionalidad común en aplicaciones web que permite a los usuarios cargar documentos, imágenes u otros tipos de ficheros al servidor. PHP proporciona un mecanismo robusto para gestionar esta operación de forma segura a través la variable superglobal $_FILES, la cual es un array asociativo con múltiples datos.
Para implementar la funcionalidad de subida de ficheros necesitamos dos elementos: el formulario y el script PHP que procese toda la información. Pueden estar en ficheros diferentes o todo en el mismo fichero.
Formulario HTML
Para permitir la subida de archivos, el formulario HTML debe cumplir tres requisitos esenciales:
- El atributo
methoddebe serPOST. - El atributo
enctypedebe establecerse comomultipart/form-data. - Debe incluir un campo
<input type="file">.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Subida de Archivos</title>
</head>
<body>
<h2>Formulario de Subida</h2>
<form action="procesar_subida.php" method="POST" enctype="multipart/form-data">
<!-- Subir un único archivo -->
<label for="archivo">Selecciona un archivo:</label>
<input type="file" name="archivo" id="archivo" required>
<!-- Subir múltiples archivos -->
<label for="archivos">Selecciona archivos:</label>
<input type="file" name="archivos[]" multiple>
<button type="submit">Subir Archivo</button>
</form>
</body>
</html>
El atributo enctype="multipart/form-data" es crucial porque indica que el formulario enviará datos binarios, no solo texto plano.
Script PHP
El archivo PHP que procesa la subida accede a la información del fichero a través del array $_FILES:
<?php
// Verificar que se ha enviado un archivo
if (isset($_FILES['archivo']) && $_FILES['archivo']['error'] === UPLOAD_ERR_OK) {
// Directorio de destino
$directorioDestino = 'uploads/';
// Crear el directorio si no existe
if (!file_exists($directorioDestino)) {
mkdir($directorioDestino, 0755, true);
}
// Información del archivo
$nombreTemporal = $_FILES['archivo']['tmp_name'];
$nombreOriginal = $_FILES['archivo']['name'];
$tamano = $_FILES['archivo']['size'];
$tipo = $_FILES['archivo']['type'];
// Generar un nombre único para evitar sobrescrituras
$nombreUnico = uniqid() . '_' . basename($nombreOriginal);
$rutaDestino = $directorioDestino . $nombreUnico;
// Validaciones adicionales
$extensionesPermitidas = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
$extension = strtolower(pathinfo($nombreOriginal, PATHINFO_EXTENSION));
if (!in_array($extension, $extensionesPermitidas)) {
die("Error: Tipo de archivo no permitido.");
}
// Mover el archivo del directorio temporal al destino final
if (move_uploaded_file($nombreTemporal, $rutaDestino)) {
echo "Archivo subido correctamente: " . htmlspecialchars($nombreUnico);
} else {
echo "Error al mover el archivo.";
}
} else {
echo "Error en la subida del archivo.";
if (isset($_FILES['archivo']['error'])) {
echo " Código de error: " . $_FILES['archivo']['error'];
}
}
?>
Parámetros del php.ini
PHP tiene varios parámetros de configuración en php.ini que controlan el comportamiento de la subida de archivos. Para comprobar cuáles son sus valores, podemos consular el php.ini o ejecutar el siguiente código PHP:
<?php
echo ini_get('file_uploads'), PHP_EOL;
echo ini_get('upload_max_filesize'), PHP_EOL;
echo ini_get('post_max_size'), PHP_EOL;
echo ini_get('max_file_uploads'), PHP_EOL;
echo ini_get('upload_tmp_dir'), PHP_EOL;
echo ini_get('max_input_time'), PHP_EOL;
echo ini_get('max_execution_time'), PHP_EOL;
file_uploads
file_uploads = On
Habilita o deshabilita la subida de archivos vía HTTP. Debe estar en On para permitir uploads.
upload_max_filesize
upload_max_filesize = 2M
Define el tamaño máximo permitido para un archivo individual. Valores comunes: 2M, 10M, 50M.
post_max_size
post_max_size = 8M
Establece el tamaño máximo de datos POST. Debe ser mayor que upload_max_filesize porque incluye el archivo más otros datos del formulario.
max_file_uploads
max_file_uploads = 20
Número máximo de archivos que se pueden subir simultáneamente en una sola petición.
upload_tmp_dir
upload_tmp_dir = "/tmp"
Directorio temporal donde PHP almacena los archivos antes de moverlos a su ubicación final. Si no se especifica, usa el directorio temporal del sistema.
max_input_time
max_input_time = 60
Tiempo máximo (en segundos) que un script puede dedicar a recibir datos de entrada.
max_execution_time
max_execution_time = 30
Tiempo máximo de ejecución de un script. Importante para archivos grandes que tardan en procesarse.
La variable $_FILES
Cuando se sube un archivo, PHP almacena su información en el array superglobal $_FILES. Si el campo del formulario se llama archivo, la estructura sería:
$_FILES['archivo'] = [
'name' => 'documento.pdf', // Nombre original del archivo
'type' => 'application/pdf', // Tipo MIME reportado por el navegador
'tmp_name' => '/tmp/phpXXXXXX', // Ruta temporal en el servidor
'error' => 0, // Código de error (0 = sin errores)
'size' => 1024000 // Tamaño en bytes
];
Descripción de cada campo:
name: Nombre original del archivo en el ordenador del usuario.type: Tipo MIME del archivo según el navegador (no es totalmente fiable, debe validarse en el servidor).tmp_name: Ruta completa del archivo temporal en el servidor donde PHP almacena inicialmente el archivo subido.error: Código de error que indica el estado de la subida.size: Tamaño del archivo en bytes.
Múltiples archivos
Si el campo permite múltiples archivos (name="archivos[]"), $_FILES tiene una estructura diferente:
$_FILES['archivos'] = [
'name' => ['archivo1.jpg', 'archivo2.pdf'],
'type' => ['image/jpeg', 'application/pdf'],
'tmp_name' => ['/tmp/phpXXXXX1', '/tmp/phpXXXXX2'],
'error' => [0, 0],
'size' => [256000, 512000]
];
Códigos de error
El campo $_FILES['archivo']['error'] contiene un código que indica el resultado de la operación. Las constantes predefinidas son:
UPLOAD_ERR_OK (0)
No hay error, el archivo se subió correctamente.
UPLOAD_ERR_INI_SIZE (1)
El archivo excede el tamaño definido en upload_max_filesize del php.ini.
if ($_FILES['archivo']['error'] === UPLOAD_ERR_INI_SIZE) {
echo "El archivo es demasiado grande (límite del servidor).";
}
UPLOAD_ERR_FORM_SIZE (2)
El archivo excede el tamaño especificado en el campo MAX_FILE_SIZE del formulario HTML.
<input type="hidden" name="MAX_FILE_SIZE" value="1000000">
<input type="file" name="archivo">
UPLOAD_ERR_PARTIAL (3)
El archivo se subió parcialmente (la transferencia se interrumpió).
UPLOAD_ERR_NO_FILE (4)
No se subió ningún archivo. Útil para campos opcionales.
UPLOAD_ERR_NO_TMP_DIR (6)
Falta el directorio temporal en el servidor.
UPLOAD_ERR_CANT_WRITE (7)
Error al escribir el archivo en disco (problemas de permisos).
UPLOAD_ERR_EXTENSION (8)
Una extensión de PHP detuvo la subida del archivo.
Ejemplo completo con manejo de errores
<?php
function obtenerMensajeError($codigoError) {
$mensajes = [
UPLOAD_ERR_OK => 'Subida exitosa',
UPLOAD_ERR_INI_SIZE => 'El archivo excede upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'El archivo excede MAX_FILE_SIZE del formulario',
UPLOAD_ERR_PARTIAL => 'El archivo se subió parcialmente',
UPLOAD_ERR_NO_FILE => 'No se seleccionó ningún archivo',
UPLOAD_ERR_NO_TMP_DIR => 'Falta el directorio temporal',
UPLOAD_ERR_CANT_WRITE => 'Error al escribir en disco',
UPLOAD_ERR_EXTENSION => 'Subida detenida por una extensión PHP'
];
return $mensajes[$codigoError] ?? 'Error desconocido';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['archivo'])) {
die("No se recibió ningún archivo.");
}
$error = $_FILES['archivo']['error'];
if ($error !== UPLOAD_ERR_OK) {
die("Error en la subida: " . obtenerMensajeError($error));
}
// Validaciones de seguridad
$tamanoMaximo = 5 * 1024 * 1024; // 5 MB
if ($_FILES['archivo']['size'] > $tamanoMaximo) {
die("El archivo es demasiado grande (máximo 5 MB).");
}
// Validar tipo MIME real (no confiar solo en $_FILES['archivo']['type'])
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$tipoMime = finfo_file($finfo, $_FILES['archivo']['tmp_name']);
finfo_close($finfo);
$tiposPermitidos = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!in_array($tipoMime, $tiposPermitidos)) {
die("Tipo de archivo no permitido.");
}
// Procesar el archivo
$directorioDestino = 'uploads/';
$nombreArchivo = uniqid() . '_' . basename($_FILES['archivo']['name']);
$rutaCompleta = $directorioDestino . $nombreArchivo;
if (move_uploaded_file($_FILES['archivo']['tmp_name'], $rutaCompleta)) {
echo "Archivo subido exitosamente: " . htmlspecialchars($nombreArchivo);
} else {
echo "Error al mover el archivo al destino final.";
}
}
?>
Consideraciones de seguridad
- Validar siempre el tipo de archivo: No confiar únicamente en la extensión o en
$_FILES['archivo']['type']. Usarfinfo_file()omime_content_type(). - Limitar el tamaño: Establecer límites razonables tanto en
php.inicomo en la lógica de la aplicación. - Renombrar archivos: Nunca usar directamente el nombre original del archivo para evitar ataques de path traversal.
- Almacenar fuera del document root: Idealmente, guardar los archivos en directorios no accesibles directamente por HTTP.
- Validar permisos: Asegurar que el directorio de destino tiene los permisos correctos (normalmente 755 para directorios, 644 para archivos).
- Sanitizar nombres: Eliminar caracteres especiales y espacios de los nombres de archivo.
- Verificar
is_uploaded_file(): Antes demove_uploaded_file(), se puede usaris_uploaded_file()para confirmar que el archivo fue realmente subido vía HTTP POST.