Real-time file upload tracking in PHP with session upload progress
Tracking file upload progress is crucial for providing a good user experience, especially when handling large files. PHP offers a built-in solution through its Session Upload Progress functionality, which provides reliable and efficient progress tracking without additional dependencies.
Server configuration requirements
Before implementing upload progress tracking, ensure your server is properly configured:
; 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
Allow multipart overhead above the 10 MiB per-file limit. For Nginx with PHP-FPM, add this to the
existing PHP location that serves upload.php, alongside its fastcgi_pass and FastCGI parameters:
client_max_body_size 12M;
client_body_buffer_size 128k;
fastcgi_request_buffering off;
proxy_request_buffering applies to HTTP proxying, not PHP-FPM. Upstream proxies must also pass
the request through without fully buffering it. Provision /var/lib/php-upload-demo outside the
webroot, owned by the PHP worker with mode 0700; do not expose it through a web-server alias.
Implementing the upload handler
Create a secure upload handler that processes files and implements proper validation:
<?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;
}
?>
Progress tracking implementation
Create a progress tracking endpoint that safely retrieves upload progress:
<?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);
Client-side integration
Implement a responsive upload form with progress tracking:
<!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>
Security caveats
MIME detection restricts accepted formats but does not prove a file harmless. Keep uploaded content private until any required scanning or processing is complete. This example tracks progress; it does not provide resumable uploads or application-specific authentication and authorization.
Browser compatibility
This implementation works in all modern browsers that support:
- FormData API
- Fetch API
- CSS transitions
- ES6+ JavaScript features
For older browsers, consider adding polyfills or using a more traditional XMLHttpRequest approach.
Summary
PHP's Session Upload Progress provides a reliable way to track file uploads without external dependencies. By combining it with proper security measures and error handling, you can create a robust upload system that provides real-time feedback to users.
For more advanced file upload capabilities, including chunked uploads and resume functionality, consider exploring the tus protocol implementation for PHP.
Happy coding!
