Files
michaelschiemer/src/Framework/LiveComponents/docs/UPLOAD-GUIDE.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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**.