Seguimiento en tiempo real de subidas de archivos en PHP con el progreso de subida en sesión
Hacer un seguimiento del progreso de subida de archivos es fundamental para ofrecer una buena experiencia de usuario, sobre todo al manejar archivos grandes. PHP ofrece una solución integrada mediante su funcionalidad de progreso de subida en sesión, que proporciona un seguimiento del progreso fiable y eficiente sin dependencias adicionales.
Requisitos de configuración del servidor
Antes de implementar el seguimiento del progreso de subida, asegúrate de que tu servidor esté configurado correctamente:
; php.ini configuration
session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
upload_max_filesize = 10M
post_max_size = 12M
Permite la sobrecarga de multipart por encima del límite de 10 MiB por archivo. En Nginx con
PHP-FPM, añade esto a la location de PHP existente que sirve upload.php, junto a su fastcgi_pass y sus parámetros de FastCGI:
client_max_body_size 12M;
client_body_buffer_size 128k;
fastcgi_request_buffering off;
proxy_request_buffering se aplica al proxy HTTP, no a PHP-FPM. Los proxies upstream también
deben dejar pasar la petición sin almacenarla por completo en búfer. Aprovisiona /var/lib/php-upload-demo fuera del
webroot, propiedad del worker de PHP y con el modo 0700; no lo expongas mediante un alias del servidor web.
Implementar el gestor de subidas
Crea un gestor de subidas seguro que procese los archivos e implemente una validación adecuada:
<?php
// upload.php
declare(strict_types=1);
session_start();
session_write_close();
class FileUploadHandler {
private const EXTENSIONS = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
private const MAX_FILE_SIZE = 10485760; // 10 MiB
private const UPLOAD_DIR = '/var/lib/php-upload-demo';
public function __construct() {
if (!is_dir(self::UPLOAD_DIR) || !is_writable(self::UPLOAD_DIR)) {
throw new RuntimeException('Private upload storage is not available');
}
}
public function handleUpload(): array {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return ['error' => 'Invalid request method'];
}
if (!isset($_FILES['file'])) {
return ['error' => 'No file uploaded'];
}
$file = $_FILES['file'];
try {
$extension = $this->validateUpload($file);
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = self::UPLOAD_DIR . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new RuntimeException('Failed to move uploaded file');
}
chmod($destination, 0600);
return [
'success' => true,
'filename' => $filename,
'size' => $file['size']
];
} catch (Exception $e) {
error_log('Upload failed: ' . get_class($e));
http_response_code(400);
return ['error' => 'The file could not be uploaded. Check its type and size.'];
}
}
private function validateUpload(array $file): string {
if (!isset($file['error'], $file['size'], $file['tmp_name']) ||
!is_int($file['error']) || !is_int($file['size']) || !is_string($file['tmp_name'])) {
throw new RuntimeException('Invalid upload fields');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException($this->getUploadErrorMessage($file['error']));
}
if ($file['size'] > self::MAX_FILE_SIZE) {
throw new RuntimeException('File exceeds maximum size limit');
}
if (!is_uploaded_file($file['tmp_name'])) {
throw new RuntimeException('Invalid upload source');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!isset(self::EXTENSIONS[$mimeType])) {
throw new RuntimeException('Invalid file type');
}
return self::EXTENSIONS[$mimeType];
}
private function getUploadErrorMessage(int $error): string {
return match($error) {
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload',
default => 'Unknown upload error'
};
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
try {
$handler = new FileUploadHandler();
echo json_encode($handler->handleUpload(), JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
error_log('Upload handler failed: ' . get_class($e));
http_response_code(500);
echo json_encode(['error' => 'The upload service is unavailable.']);
}
exit;
}
?>
Implementación del seguimiento del progreso
Crea un endpoint de seguimiento que recupere de forma segura el progreso de la subida:
<?php
// progress.php
declare(strict_types=1);
session_start();
header('Content-Type: application/json');
$id = $_GET['id'] ?? '';
$key = ini_get('session.upload_progress.prefix') . (is_string($id) ? $id : '');
$progress = [];
if (isset($_SESSION[$key])) {
$current = $_SESSION[$key];
$total = (int) $current['content_length'];
$progress = [
'lengthComputable' => $total > 0,
'loaded' => $current['bytes_processed'],
'total' => $total,
'percentage' => $total > 0 ? min(100, ($current['bytes_processed'] / $total) * 100) : 0
];
}
session_write_close();
echo json_encode($progress);
Integración del lado del cliente
Implementa un formulario de subida adaptable con seguimiento del progreso:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>File Upload with Progress</title>
<style>
.progress {
width: 100%;
height: 20px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
width: 0;
height: 100%;
background: #4caf50;
transition: width 0.3s ease;
}
</style>
</head>
<body>
<form id="uploadForm" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" id="progress-id" />
<input type="file" name="file" required />
<button type="submit">Upload</button>
<div class="progress">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div id="status"></div>
</form>
<script>
const form = document.getElementById('uploadForm')
const progressBar = document.getElementById('progress-bar')
const status = document.getElementById('status')
const progressId = document.getElementById('progress-id')
form.addEventListener('submit', async (e) => {
e.preventDefault()
// Generate unique ID for this upload
const uploadId = Date.now().toString()
progressId.value = uploadId
const formData = new FormData(form)
let tracker
try {
// Establish the session cookie before the upload and polling requests.
const sessionResponse = await fetch('progress.php')
if (!sessionResponse.ok) throw new Error('Could not initialize upload tracking')
// Start progress tracking
tracker = trackProgress(uploadId)
// Perform upload
const response = await fetch('upload.php', {
method: 'POST',
body: formData,
})
if (!response.ok) throw new Error('Upload failed. Check the file type and size.')
const result = await response.json()
if (result.error) {
throw new Error(result.error)
}
status.textContent = 'Upload complete!'
progressBar.style.width = '100%'
} catch (error) {
status.textContent = `Error: ${error.message}`
progressBar.style.backgroundColor = '#f44336'
} finally {
clearInterval(tracker)
}
})
function trackProgress(uploadId) {
return setInterval(async () => {
try {
const response = await fetch(`progress.php?id=${uploadId}`)
if (!response.ok) throw new Error('Could not read upload progress')
const progress = await response.json()
if (progress.lengthComputable) {
const percentage = Math.round(progress.percentage)
progressBar.style.width = `${percentage}%`
status.textContent = `Uploading: ${percentage}%`
}
} catch (error) {
console.error('Progress tracking error:', error)
}
}, 1000)
}
</script>
</body>
</html>
Advertencias de seguridad
La detección de MIME restringe los formatos aceptados, pero no demuestra que un archivo sea inofensivo. Mantén privado el contenido subido hasta que se complete cualquier análisis o procesamiento necesario. Este ejemplo hace un seguimiento del progreso; no ofrece subidas reanudables ni autenticación y autorización específicas de la aplicación.
Compatibilidad con navegadores
Esta implementación funciona en todos los navegadores modernos que admiten:
- API FormData
- API Fetch
- Transiciones CSS
- Funcionalidades de JavaScript ES6+
Para navegadores más antiguos, considera añadir polyfills o usar un enfoque más tradicional con XMLHttpRequest.
Resumen
El progreso de subida en sesión de PHP ofrece una forma fiable de hacer seguimiento de las subidas de archivos sin dependencias externas. Si lo combinas con medidas de seguridad adecuadas y un manejo correcto de los errores, puedes crear un sistema de subidas robusto que ofrece a los usuarios información en tiempo real.
Para funcionalidades de subida de archivos más avanzadas, incluidas las subidas por fragmentos y la reanudación, considera explorar la implementación del protocolo tus para PHP.
¡Feliz programación!
