- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
954 lines
25 KiB
Markdown
954 lines
25 KiB
Markdown
# LiveComponents Upload Guide
|
|
|
|
Comprehensive guide for implementing chunked, resumable file uploads with progress tracking and virus scanning.
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Quick Start](#quick-start)
|
|
- [Chunked Upload Architecture](#chunked-upload-architecture)
|
|
- [Backend Implementation](#backend-implementation)
|
|
- [Client-Side Implementation](#client-side-implementation)
|
|
- [Progress Tracking](#progress-tracking)
|
|
- [Quarantine & Virus Scanning](#quarantine--virus-scanning)
|
|
- [Resume Capability](#resume-capability)
|
|
- [Security](#security)
|
|
- [Testing](#testing)
|
|
- [Production Deployment](#production-deployment)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
LiveComponents provides a **production-ready chunked upload system** with:
|
|
|
|
✅ **Chunked Uploads** - Split large files into manageable chunks (512KB default)
|
|
✅ **Resume Capability** - Resume interrupted uploads from last chunk
|
|
✅ **Integrity Verification** - SHA-256 hashing on client and server
|
|
✅ **Progress Tracking** - Real-time progress via Server-Sent Events (SSE)
|
|
✅ **Quarantine System** - Automatic virus scanning integration
|
|
✅ **Parallel Uploads** - Concurrent chunk uploads (3 parallel by default)
|
|
✅ **Retry Logic** - Automatic retry with exponential backoff
|
|
✅ **Memory Efficient** - Stream-based chunk assembly
|
|
|
|
### Use Cases
|
|
|
|
- **Large File Uploads**: Videos, images, documents (>100MB)
|
|
- **Unreliable Networks**: Mobile connections, slow networks
|
|
- **Background Uploads**: Long-running uploads with resume capability
|
|
- **Secure Uploads**: Virus scanning and integrity verification
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### 1. HTML Template
|
|
|
|
```html
|
|
<div data-lc-component="file-uploader" data-lc-id="{component_id}">
|
|
<h3>Upload Files</h3>
|
|
|
|
<!-- File input -->
|
|
<input
|
|
type="file"
|
|
id="file-upload-input"
|
|
accept="image/*,video/*,.pdf,.doc,.docx"
|
|
multiple
|
|
/>
|
|
|
|
<!-- Upload button -->
|
|
<button id="upload-btn" disabled>Upload</button>
|
|
|
|
<!-- Progress display -->
|
|
<div id="upload-progress" class="hidden">
|
|
<div class="progress-bar">
|
|
<div id="progress-fill" style="width: 0%"></div>
|
|
</div>
|
|
<p id="progress-text">0%</p>
|
|
<p id="upload-status">Preparing upload...</p>
|
|
</div>
|
|
|
|
<!-- Uploaded files list -->
|
|
<div id="uploaded-files">
|
|
<h4>Uploaded Files</h4>
|
|
<ul data-lc-fragment="file-list">
|
|
<for items="uploaded_files" as="file">
|
|
<li>
|
|
{file.name} ({file.size_formatted})
|
|
<span class="status">{file.status}</span>
|
|
</li>
|
|
</for>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 2. JavaScript Integration
|
|
|
|
```javascript
|
|
import { ChunkedUploader } from '@js/modules/livecomponent';
|
|
|
|
const fileInput = document.getElementById('file-upload-input');
|
|
const uploadBtn = document.getElementById('upload-btn');
|
|
const progressBar = document.getElementById('progress-fill');
|
|
const progressText = document.getElementById('progress-text');
|
|
|
|
// Enable button when file selected
|
|
fileInput.addEventListener('change', (e) => {
|
|
uploadBtn.disabled = e.target.files.length === 0;
|
|
});
|
|
|
|
// Initialize uploader
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
chunkSize: 512 * 1024, // 512KB chunks
|
|
maxConcurrentChunks: 3, // 3 parallel uploads
|
|
maxRetries: 3, // Retry failed chunks 3 times
|
|
enableSSE: true, // Real-time progress via SSE
|
|
|
|
// Progress callback
|
|
onProgress: (progress) => {
|
|
progressBar.style.width = `${progress}%`;
|
|
progressText.textContent = `${progress.toFixed(1)}%`;
|
|
},
|
|
|
|
// Complete callback
|
|
onComplete: (result) => {
|
|
alert(`Upload complete! File ID: ${result.fileId}`);
|
|
fileInput.value = '';
|
|
uploadBtn.disabled = true;
|
|
},
|
|
|
|
// Error callback
|
|
onError: (error) => {
|
|
alert(`Upload failed: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// Upload on button click
|
|
uploadBtn.addEventListener('click', async () => {
|
|
const file = fileInput.files[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const result = await uploader.uploadFile(file);
|
|
console.log('Upload successful:', result);
|
|
} catch (error) {
|
|
console.error('Upload failed:', error);
|
|
}
|
|
});
|
|
```
|
|
|
|
### 3. Component Backend
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Application\LiveComponents\FileUploader;
|
|
|
|
use App\Framework\LiveComponents\Attributes\Action;
|
|
use App\Framework\LiveComponents\Attributes\LiveComponent;
|
|
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
|
|
|
#[LiveComponent('file-uploader')]
|
|
final readonly class FileUploaderComponent implements LiveComponentContract
|
|
{
|
|
public function __construct(
|
|
public ComponentId $id,
|
|
public FileUploaderState $state,
|
|
private FileRepository $fileRepository
|
|
) {}
|
|
|
|
#[Action]
|
|
public function refreshFileList(): FileUploaderState
|
|
{
|
|
$files = $this->fileRepository->findByUser($this->state->userId);
|
|
|
|
return $this->state->withFiles($files);
|
|
}
|
|
|
|
public function render(): string
|
|
{
|
|
return 'components/file-uploader';
|
|
}
|
|
}
|
|
```
|
|
|
|
**That's it!** The framework handles chunking, hashing, upload, assembly, and progress tracking automatically.
|
|
|
|
---
|
|
|
|
## Chunked Upload Architecture
|
|
|
|
### Upload Lifecycle
|
|
|
|
```
|
|
1. Initialize Session
|
|
├─ Generate unique session ID
|
|
├─ Calculate total chunks
|
|
├─ Store session metadata
|
|
└─ Return session ID to client
|
|
|
|
2. Upload Chunks (parallel)
|
|
├─ Hash chunk on client (SHA-256)
|
|
├─ Upload chunk with hash
|
|
├─ Verify hash on server
|
|
├─ Store chunk temporarily
|
|
└─ Update session progress
|
|
|
|
3. Complete Upload
|
|
├─ Assemble all chunks
|
|
├─ Verify final file hash
|
|
├─ Move to quarantine
|
|
├─ Trigger virus scan
|
|
└─ Return file ID
|
|
|
|
4. Post-Processing
|
|
├─ Virus scan (async)
|
|
├─ Move from quarantine
|
|
├─ Generate thumbnails
|
|
└─ Update component state
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
| Endpoint | Method | Purpose |
|
|
|----------|--------|---------|
|
|
| `/live-component/upload/init` | POST | Initialize upload session |
|
|
| `/live-component/upload/chunk` | POST | Upload single chunk |
|
|
| `/live-component/upload/complete` | POST | Finalize upload |
|
|
| `/live-component/upload/abort` | POST | Cancel upload |
|
|
| `/live-component/upload/status/{sessionId}` | GET | Get upload status |
|
|
|
|
---
|
|
|
|
## Backend Implementation
|
|
|
|
### Upload Session Management
|
|
|
|
The framework automatically manages upload sessions via `UploadSessionStore`:
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Upload\UploadSession;
|
|
use App\Framework\LiveComponents\Upload\UploadSessionId;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
|
|
// Framework creates sessions automatically
|
|
// You can query session status:
|
|
|
|
$sessionId = UploadSessionId::fromString($sessionIdString);
|
|
$session = $this->uploadSessionStore->get($sessionId);
|
|
|
|
if ($session === null) {
|
|
// Session expired or doesn't exist
|
|
}
|
|
|
|
// Session contains:
|
|
$session->id; // UploadSessionId
|
|
$session->fileName; // Original filename
|
|
$session->totalSize; // Total file size
|
|
$session->totalChunks; // Number of chunks
|
|
$session->uploadedChunks; // Array of uploaded chunk indices
|
|
$session->componentId; // Associated component ID
|
|
$session->userId; // User ID (if authenticated)
|
|
$session->createdAt; // Session creation time
|
|
$session->expiresAt; // Session expiration
|
|
```
|
|
|
|
### Chunk Integrity Verification
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Upload\IntegrityValidator;
|
|
use App\Framework\LiveComponents\Upload\ChunkHash;
|
|
|
|
// Framework automatically verifies chunk integrity
|
|
// But you can also use IntegrityValidator manually:
|
|
|
|
$validator = $container->get(IntegrityValidator::class);
|
|
|
|
// Verify chunk hash
|
|
$expectedHash = ChunkHash::fromHex($clientProvidedHash);
|
|
$isValid = $validator->verifyChunkHash($chunkPath, $expectedHash);
|
|
|
|
if (!$isValid) {
|
|
throw new ChunkIntegrityException('Chunk hash mismatch');
|
|
}
|
|
|
|
// Verify final file hash
|
|
$fileHash = $validator->hashFile($assembledFilePath);
|
|
$isValid = $validator->verifyFileHash(
|
|
$assembledFilePath,
|
|
ChunkHash::fromHex($expectedFileHash)
|
|
);
|
|
```
|
|
|
|
### Custom Post-Processing
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Upload\ChunkedUploadManager;
|
|
use App\Framework\Event\EventHandler;
|
|
use App\Framework\LiveComponents\Upload\Events\UploadCompletedEvent;
|
|
|
|
#[EventHandler]
|
|
final readonly class ProcessUploadedFileHandler
|
|
{
|
|
public function handle(UploadCompletedEvent $event): void
|
|
{
|
|
// File is in quarantine, waiting for virus scan
|
|
// You can process metadata here
|
|
|
|
$file = new File(
|
|
id: $event->fileId,
|
|
originalName: $event->originalFileName,
|
|
size: $event->fileSize,
|
|
uploadedBy: $event->userId,
|
|
componentId: $event->componentId
|
|
);
|
|
|
|
$this->fileRepository->save($file);
|
|
|
|
// Generate thumbnails (if image)
|
|
if ($this->isImage($event->mimeType)) {
|
|
$this->thumbnailGenerator->generate($event->filePath);
|
|
}
|
|
|
|
// Extract metadata (if video)
|
|
if ($this->isVideo($event->mimeType)) {
|
|
$this->videoMetadataExtractor->extract($event->filePath);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Storage Configuration
|
|
|
|
```php
|
|
// Environment configuration
|
|
UPLOAD_TEMP_DIR=/var/www/storage/uploads/temp
|
|
UPLOAD_QUARANTINE_DIR=/var/www/storage/uploads/quarantine
|
|
UPLOAD_FINAL_DIR=/var/www/storage/uploads/files
|
|
UPLOAD_MAX_FILE_SIZE=2147483648 # 2GB
|
|
UPLOAD_CHUNK_SIZE=524288 # 512KB
|
|
UPLOAD_SESSION_TTL=3600 # 1 hour
|
|
```
|
|
|
|
---
|
|
|
|
## Client-Side Implementation
|
|
|
|
### Basic Upload
|
|
|
|
```javascript
|
|
import { ChunkedUploader } from '@js/modules/livecomponent';
|
|
|
|
const uploader = new ChunkedUploader('file-uploader:main');
|
|
|
|
// Upload a file
|
|
const result = await uploader.uploadFile(file);
|
|
|
|
console.log('File uploaded:', result.fileId);
|
|
```
|
|
|
|
### Advanced Configuration
|
|
|
|
```javascript
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
// Chunk configuration
|
|
chunkSize: 512 * 1024, // 512KB chunks (default)
|
|
maxConcurrentChunks: 3, // 3 parallel uploads (default)
|
|
|
|
// Retry configuration
|
|
maxRetries: 3, // Retry failed chunks (default: 3)
|
|
retryDelay: 1000, // Initial retry delay (default: 1s)
|
|
retryBackoffMultiplier: 2, // Exponential backoff (default: 2)
|
|
|
|
// Progress tracking
|
|
enableSSE: true, // Real-time progress via SSE (default: true)
|
|
progressUpdateInterval: 100, // Progress callback interval (default: 100ms)
|
|
|
|
// Callbacks
|
|
onProgress: (progress) => {
|
|
console.log(`Upload: ${progress.toFixed(2)}%`);
|
|
},
|
|
|
|
onChunkUpload: (chunkIndex, totalChunks) => {
|
|
console.log(`Chunk ${chunkIndex + 1}/${totalChunks} uploaded`);
|
|
},
|
|
|
|
onComplete: (result) => {
|
|
console.log('Upload complete:', result);
|
|
},
|
|
|
|
onError: (error) => {
|
|
console.error('Upload failed:', error);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Multiple File Upload
|
|
|
|
```javascript
|
|
const files = Array.from(fileInput.files);
|
|
|
|
// Sequential uploads
|
|
for (const file of files) {
|
|
try {
|
|
const result = await uploader.uploadFile(file);
|
|
console.log(`${file.name} uploaded successfully`);
|
|
} catch (error) {
|
|
console.error(`${file.name} failed:`, error);
|
|
}
|
|
}
|
|
|
|
// Parallel uploads (careful with concurrency)
|
|
const uploads = files.map(file => uploader.uploadFile(file));
|
|
const results = await Promise.allSettled(uploads);
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
console.log(`${files[index].name} uploaded`);
|
|
} else {
|
|
console.error(`${files[index].name} failed:`, result.reason);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Custom Progress UI
|
|
|
|
```javascript
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
onProgress: (progress) => {
|
|
// Update progress bar
|
|
progressBar.style.width = `${progress}%`;
|
|
progressText.textContent = `${progress.toFixed(1)}%`;
|
|
|
|
// Update status text
|
|
if (progress < 25) {
|
|
statusText.textContent = 'Starting upload...';
|
|
} else if (progress < 75) {
|
|
statusText.textContent = 'Uploading...';
|
|
} else if (progress < 100) {
|
|
statusText.textContent = 'Almost done...';
|
|
}
|
|
},
|
|
|
|
onChunkUpload: (chunkIndex, totalChunks) => {
|
|
chunksText.textContent = `${chunkIndex + 1} / ${totalChunks} chunks`;
|
|
},
|
|
|
|
onComplete: (result) => {
|
|
progressBar.classList.add('complete');
|
|
statusText.textContent = 'Upload complete!';
|
|
|
|
// Show success message
|
|
setTimeout(() => {
|
|
resetUploadUI();
|
|
}, 2000);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Progress Tracking
|
|
|
|
### Real-Time Progress via SSE
|
|
|
|
The framework automatically broadcasts upload progress via Server-Sent Events:
|
|
|
|
```javascript
|
|
// SSE automatically enabled by default
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
enableSSE: true, // Real-time progress
|
|
|
|
onProgress: (progress) => {
|
|
// Called automatically as chunks upload
|
|
console.log(`Progress: ${progress}%`);
|
|
}
|
|
});
|
|
|
|
// Progress events are automatically synchronized between:
|
|
// - Chunk upload completion
|
|
// - Server-side progress updates
|
|
// - SSE broadcasts
|
|
```
|
|
|
|
### Manual Progress Polling
|
|
|
|
```javascript
|
|
// Disable SSE and poll manually
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
enableSSE: false // Disable automatic SSE
|
|
});
|
|
|
|
// Poll upload status
|
|
const sessionId = 'upload-session-id';
|
|
|
|
async function pollUploadStatus() {
|
|
const response = await fetch(`/live-component/upload/status/${sessionId}`);
|
|
const status = await response.json();
|
|
|
|
console.log('Upload progress:', {
|
|
uploaded: status.uploaded_chunks,
|
|
total: status.total_chunks,
|
|
progress: (status.uploaded_chunks / status.total_chunks) * 100
|
|
});
|
|
|
|
if (status.uploaded_chunks < status.total_chunks) {
|
|
setTimeout(pollUploadStatus, 1000); // Poll every second
|
|
}
|
|
}
|
|
```
|
|
|
|
### Progress Events
|
|
|
|
```javascript
|
|
// Listen to granular progress events
|
|
uploader.on('upload:started', (data) => {
|
|
console.log('Upload started:', data.fileName);
|
|
});
|
|
|
|
uploader.on('chunk:uploaded', (data) => {
|
|
console.log(`Chunk ${data.chunkIndex} uploaded`);
|
|
});
|
|
|
|
uploader.on('upload:progress', (data) => {
|
|
console.log(`Progress: ${data.progress}%`);
|
|
});
|
|
|
|
uploader.on('upload:complete', (data) => {
|
|
console.log('Upload complete:', data.fileId);
|
|
});
|
|
|
|
uploader.on('upload:error', (error) => {
|
|
console.error('Upload error:', error);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Quarantine & Virus Scanning
|
|
|
|
### How Quarantine Works
|
|
|
|
```
|
|
1. Upload Complete
|
|
↓
|
|
2. File Assembled
|
|
↓
|
|
3. Move to Quarantine (automatic)
|
|
├─ File stored in quarantine directory
|
|
├─ QuarantineStatus: PENDING
|
|
└─ Virus scan triggered (async)
|
|
↓
|
|
4. Virus Scan
|
|
├─ ClamAV / VirusTotal integration
|
|
├─ QuarantineStatus: SCANNING
|
|
└─ Scan result returned
|
|
↓
|
|
5. Post-Scan
|
|
├─ Clean → Move to final storage, QuarantineStatus: RELEASED
|
|
└─ Infected → Delete file, QuarantineStatus: QUARANTINED
|
|
```
|
|
|
|
### Virus Scanner Integration
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Upload\QuarantineService;
|
|
use App\Framework\LiveComponents\Upload\ScanResult;
|
|
use App\Framework\LiveComponents\Upload\ScanStatus;
|
|
|
|
// Custom virus scanner implementation
|
|
final readonly class ClamAVScanner implements VirusScanner
|
|
{
|
|
public function scan(string $filePath): ScanResult
|
|
{
|
|
// Execute ClamAV scan
|
|
$output = shell_exec("clamscan --no-summary {$filePath}");
|
|
|
|
$isClean = str_contains($output, 'OK');
|
|
|
|
if ($isClean) {
|
|
return ScanResult::clean();
|
|
}
|
|
|
|
// Extract virus signature
|
|
preg_match('/FOUND: (.+)/', $output, $matches);
|
|
$signature = $matches[1] ?? 'Unknown';
|
|
|
|
return ScanResult::infected($signature);
|
|
}
|
|
}
|
|
|
|
// Register scanner
|
|
$container->singleton(VirusScanner::class, new ClamAVScanner());
|
|
```
|
|
|
|
### VirusTotal Integration
|
|
|
|
```php
|
|
final readonly class VirusTotalScanner implements VirusScanner
|
|
{
|
|
public function __construct(
|
|
private string $apiKey,
|
|
private HttpClient $httpClient
|
|
) {}
|
|
|
|
public function scan(string $filePath): ScanResult
|
|
{
|
|
// Upload file to VirusTotal
|
|
$response = $this->httpClient->post('https://www.virustotal.com/api/v3/files', [
|
|
'file' => new CURLFile($filePath)
|
|
], [
|
|
'x-apikey' => $this->apiKey
|
|
]);
|
|
|
|
$data = json_decode($response->body, true);
|
|
|
|
// Check scan results
|
|
$positives = $data['data']['attributes']['last_analysis_stats']['malicious'] ?? 0;
|
|
|
|
if ($positives > 0) {
|
|
return ScanResult::infected(
|
|
signature: "{$positives} engines detected malware"
|
|
);
|
|
}
|
|
|
|
return ScanResult::clean();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Quarantine Status Tracking
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Upload\QuarantineStatus;
|
|
|
|
// Check quarantine status
|
|
$status = $this->quarantineService->getStatus($fileId);
|
|
|
|
match ($status) {
|
|
QuarantineStatus::PENDING => 'Waiting for scan...',
|
|
QuarantineStatus::SCANNING => 'Scanning for viruses...',
|
|
QuarantineStatus::QUARANTINED => 'File infected and quarantined',
|
|
QuarantineStatus::RELEASED => 'File clean and available',
|
|
};
|
|
|
|
// Listen to scan completion
|
|
$this->eventDispatcher->listen(FileScanCompletedEvent::class, function($event) {
|
|
if ($event->scanResult->isClean()) {
|
|
// File is safe - notify user
|
|
$this->notificationService->send(
|
|
$event->userId,
|
|
"Your file '{$event->fileName}' has been uploaded successfully"
|
|
);
|
|
} else {
|
|
// File infected - alert security team
|
|
$this->securityLogger->alert('Infected file uploaded', [
|
|
'file_id' => $event->fileId,
|
|
'user_id' => $event->userId,
|
|
'signature' => $event->scanResult->signature
|
|
]);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Resume Capability
|
|
|
|
### How Resume Works
|
|
|
|
1. **Upload Interrupted** (network failure, browser close, etc.)
|
|
2. **User Returns** and selects same file
|
|
3. **Framework Detects** existing session by file hash
|
|
4. **Resume Upload** from last successfully uploaded chunk
|
|
|
|
### Resume Implementation
|
|
|
|
```javascript
|
|
const uploader = new ChunkedUploader('file-uploader:main', {
|
|
enableResume: true // Enable resume (default: true)
|
|
});
|
|
|
|
// Framework automatically:
|
|
// 1. Hashes file to generate consistent session ID
|
|
// 2. Checks server for existing session
|
|
// 3. Resumes from last uploaded chunk
|
|
// 4. Continues upload seamlessly
|
|
|
|
// Manual resume control
|
|
uploader.on('upload:resuming', (data) => {
|
|
console.log(`Resuming from chunk ${data.lastChunkIndex}`);
|
|
console.log(`${data.uploadedChunks}/${data.totalChunks} chunks already uploaded`);
|
|
|
|
// Show resume message to user
|
|
showNotification(`Resuming upload from ${data.progress}%`);
|
|
});
|
|
```
|
|
|
|
### Session Expiration
|
|
|
|
```php
|
|
// Configure session TTL
|
|
UPLOAD_SESSION_TTL=3600 # 1 hour
|
|
|
|
// Sessions expire after TTL
|
|
// After expiration, upload must restart from beginning
|
|
```
|
|
|
|
### Resume Best Practices
|
|
|
|
```javascript
|
|
// 1. Always use same file (don't modify between uploads)
|
|
const file = fileInput.files[0];
|
|
const result = await uploader.uploadFile(file);
|
|
|
|
// 2. Don't change filename (affects session ID)
|
|
// ❌ Bad
|
|
const renamedFile = new File([file], 'new-name.jpg', { type: file.type });
|
|
|
|
// ✅ Good
|
|
const originalFile = fileInput.files[0];
|
|
|
|
// 3. Handle resume gracefully
|
|
uploader.on('upload:resuming', (data) => {
|
|
// Inform user
|
|
alert(`Resuming upload from ${data.progress.toFixed(1)}%`);
|
|
});
|
|
|
|
uploader.on('upload:cannot-resume', (reason) => {
|
|
// Session expired or file changed
|
|
alert(`Cannot resume: ${reason}. Starting new upload...`);
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
### Upload Security Checklist
|
|
|
|
- [x] **File Size Limits** - Enforce max file size (2GB default)
|
|
- [x] **File Type Validation** - MIME type and extension checking
|
|
- [x] **Integrity Verification** - SHA-256 hashing client and server
|
|
- [x] **Virus Scanning** - Automatic quarantine and scan
|
|
- [x] **Rate Limiting** - Prevent upload abuse
|
|
- [x] **Authentication** - Only authenticated users can upload
|
|
- [x] **CSRF Protection** - Token validation on all uploads
|
|
- [x] **Idempotency** - Prevent duplicate uploads
|
|
|
|
### File Type Validation
|
|
|
|
```php
|
|
// Configure allowed file types
|
|
$allowedMimeTypes = [
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
'video/mp4',
|
|
'application/pdf',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
];
|
|
|
|
$allowedExtensions = [
|
|
'jpg', 'jpeg', 'png', 'gif', 'webp',
|
|
'mp4', 'pdf', 'doc', 'docx'
|
|
];
|
|
|
|
// Validation happens automatically in ChunkedUploadController
|
|
// But you can also validate manually:
|
|
if (!in_array($file->getMimeType(), $allowedMimeTypes)) {
|
|
throw new InvalidFileTypeException('File type not allowed');
|
|
}
|
|
|
|
if (!in_array($file->getExtension(), $allowedExtensions)) {
|
|
throw new InvalidFileTypeException('File extension not allowed');
|
|
}
|
|
```
|
|
|
|
### Size Limits
|
|
|
|
```javascript
|
|
// Client-side validation (before upload)
|
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; // 2GB
|
|
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
alert('File too large. Maximum size is 2GB.');
|
|
return;
|
|
}
|
|
|
|
// Server-side enforcement (automatic)
|
|
// Configured via UPLOAD_MAX_FILE_SIZE environment variable
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
```php
|
|
use App\Framework\LiveComponents\Attributes\Action;
|
|
|
|
#[Action(
|
|
rateLimit: 10, // Max 10 upload requests
|
|
rateLimitWindow: 3600 // Per hour
|
|
)]
|
|
public function uploadChunk(UploadChunkRequest $request): JsonResult
|
|
{
|
|
// Rate limiting applied automatically by framework
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
```php
|
|
use function Pest\LiveComponents\mountComponent;
|
|
use function Pest\LiveComponents\callAction;
|
|
|
|
describe('FileUploaderComponent', function () {
|
|
it('initializes upload session', function () {
|
|
$manager = container()->get(ChunkedUploadManager::class);
|
|
|
|
$session = $manager->initializeSession(
|
|
componentId: 'file-uploader:test',
|
|
fileName: 'test.pdf',
|
|
fileSize: 1024 * 1024, // 1MB
|
|
totalChunks: 2,
|
|
userId: '123'
|
|
);
|
|
|
|
expect($session->fileName)->toBe('test.pdf');
|
|
expect($session->totalChunks)->toBe(2);
|
|
});
|
|
|
|
it('verifies chunk integrity', function () {
|
|
$validator = container()->get(IntegrityValidator::class);
|
|
|
|
$chunkPath = __DIR__ . '/fixtures/chunk.bin';
|
|
$expectedHash = ChunkHash::fromHex(hash_file('sha256', $chunkPath));
|
|
|
|
$isValid = $validator->verifyChunkHash($chunkPath, $expectedHash);
|
|
|
|
expect($isValid)->toBeTrue();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```javascript
|
|
// JavaScript tests with Jest
|
|
describe('ChunkedUploader', () => {
|
|
it('uploads file in chunks', async () => {
|
|
const file = new File(['test content'], 'test.txt', {
|
|
type: 'text/plain'
|
|
});
|
|
|
|
const uploader = new ChunkedUploader('test:uploader', {
|
|
chunkSize: 512
|
|
});
|
|
|
|
const result = await uploader.uploadFile(file);
|
|
|
|
expect(result.fileId).toBeDefined();
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('resumes interrupted upload', async () => {
|
|
const file = new File(['test content'], 'test.txt');
|
|
const uploader = new ChunkedUploader('test:uploader');
|
|
|
|
// Start upload
|
|
const uploadPromise = uploader.uploadFile(file);
|
|
|
|
// Simulate interruption
|
|
uploader.abort();
|
|
|
|
// Resume
|
|
const result = await uploader.uploadFile(file);
|
|
|
|
expect(result.resumed).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Production Deployment
|
|
|
|
### Infrastructure Setup
|
|
|
|
```bash
|
|
# Create upload directories
|
|
mkdir -p /var/www/storage/uploads/{temp,quarantine,files}
|
|
|
|
# Set permissions
|
|
chown -R www-data:www-data /var/www/storage/uploads
|
|
chmod -R 755 /var/www/storage/uploads
|
|
|
|
# Install ClamAV
|
|
apt-get install clamav clamav-daemon
|
|
systemctl start clamav-daemon
|
|
|
|
# Update virus definitions
|
|
freshclam
|
|
```
|
|
|
|
### Environment Configuration
|
|
|
|
```env
|
|
# Upload settings
|
|
UPLOAD_TEMP_DIR=/var/www/storage/uploads/temp
|
|
UPLOAD_QUARANTINE_DIR=/var/www/storage/uploads/quarantine
|
|
UPLOAD_FINAL_DIR=/var/www/storage/uploads/files
|
|
UPLOAD_MAX_FILE_SIZE=2147483648
|
|
UPLOAD_CHUNK_SIZE=524288
|
|
UPLOAD_SESSION_TTL=3600
|
|
|
|
# Virus scanning
|
|
CLAMAV_SOCKET=/var/run/clamav/clamd.ctl
|
|
VIRUSTOTAL_API_KEY=your-api-key-here
|
|
|
|
# Cleanup
|
|
UPLOAD_CLEANUP_ENABLED=true
|
|
UPLOAD_CLEANUP_INTERVAL=3600 # Clean up old sessions every hour
|
|
```
|
|
|
|
### Monitoring
|
|
|
|
```php
|
|
// Track upload metrics
|
|
$this->metrics->increment('upload.sessions.created');
|
|
$this->metrics->increment('upload.chunks.uploaded');
|
|
$this->metrics->increment('upload.completed');
|
|
$this->metrics->increment('upload.failed');
|
|
|
|
// Track virus scan results
|
|
$this->metrics->increment('virus_scan.clean');
|
|
$this->metrics->increment('virus_scan.infected');
|
|
|
|
// Track performance
|
|
$this->metrics->timing('upload.session.duration', $duration);
|
|
$this->metrics->timing('upload.chunk.duration', $chunkDuration);
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
LiveComponents Chunked Upload System:
|
|
|
|
✅ **Production-Ready** - Battle-tested with large files (>2GB)
|
|
✅ **Resumable** - Automatic resume from interruptions
|
|
✅ **Secure** - Hash verification + virus scanning + quarantine
|
|
✅ **Fast** - Parallel chunk uploads (3 concurrent)
|
|
✅ **Reliable** - Retry logic with exponential backoff
|
|
✅ **User-Friendly** - Real-time progress via SSE
|
|
✅ **Memory-Efficient** - Stream-based chunk assembly
|
|
|
|
Perfect for applications requiring **large file uploads** with **reliability** and **security**.
|