fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
893
docs/livecomponents/livecomponent-file-uploads.md
Normal file
893
docs/livecomponents/livecomponent-file-uploads.md
Normal file
@@ -0,0 +1,893 @@
|
||||
# LiveComponent File Uploads
|
||||
|
||||
Komplette Dokumentation des File Upload Systems für LiveComponents mit Drag & Drop, Multi-File Support und Progress Tracking.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das File Upload System ermöglicht es, Dateien direkt in LiveComponents hochzuladen mit:
|
||||
|
||||
- **Drag & Drop Support** - Intuitive Dateiauswahl durch Ziehen & Ablegen
|
||||
- **Multi-File Uploads** - Mehrere Dateien gleichzeitig hochladen
|
||||
- **Progress Tracking** - Echtzeit-Fortschrittsanzeige pro Datei und gesamt
|
||||
- **Preview Funktionalität** - Bild-Vorschauen und Datei-Icons
|
||||
- **Client-Side Validation** - Validierung vor dem Upload (MIME-Type, Größe, Extension)
|
||||
- **CSRF Protection** - Automatische CSRF-Token-Integration
|
||||
- **Component State Management** - Nahtlose Integration mit LiveComponent State
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ FileUploadWidget │───▶│ ComponentFileUploader│───▶│ Backend Route │
|
||||
│ (UI Component) │ │ (Upload Manager) │ │ /upload endpoint │
|
||||
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
||||
│ │ │
|
||||
Drop Zone UI Multi-File Queue State Update
|
||||
File List Progress Tracking HTML Refresh
|
||||
Progress Bars CSRF Handling Event Dispatch
|
||||
```
|
||||
|
||||
### Komponenten
|
||||
|
||||
1. **Backend** (`src/Framework/LiveComponents/`)
|
||||
- `Controllers/LiveComponentController::handleUpload()` - Upload Route Handler
|
||||
- `LiveComponentHandler::handleUpload()` - Business Logic
|
||||
- `Contracts/SupportsFileUpload` - Interface für uploadbare Components
|
||||
- `ValueObjects/FileUploadProgress` - Progress Tracking VO
|
||||
- `ValueObjects/UploadedComponentFile` - File Metadata VO
|
||||
|
||||
2. **Frontend** (`resources/js/modules/livecomponent/`)
|
||||
- `ComponentFileUploader.js` - Core Upload Manager
|
||||
- `FileUploadWidget.js` - Pre-built UI Component
|
||||
- `DragDropZone` - Drag & Drop Handler
|
||||
- `FileValidator` - Client-Side Validation
|
||||
- `UploadProgress` - Progress Tracking
|
||||
|
||||
3. **Styling** (`resources/css/components/`)
|
||||
- `file-upload-widget.css` - Complete UI Styling
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Interface: SupportsFileUpload
|
||||
|
||||
LiveComponents, die Uploads unterstützen, müssen das `SupportsFileUpload` Interface implementieren:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Framework\LiveComponents\Contracts\SupportsFileUpload;
|
||||
use App\Framework\Http\UploadedFile;
|
||||
use App\Framework\LiveComponents\ComponentEventDispatcher;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
||||
|
||||
final class DocumentUploadComponent extends AbstractLiveComponent implements SupportsFileUpload
|
||||
{
|
||||
private array $uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* Handle file upload
|
||||
*/
|
||||
public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData
|
||||
{
|
||||
// 1. Validate file (already done by framework, but you can add custom validation)
|
||||
if (!$this->isValidDocument($file)) {
|
||||
throw new \InvalidArgumentException('Invalid document type');
|
||||
}
|
||||
|
||||
// 2. Process uploaded file
|
||||
$savedPath = $this->saveFile($file);
|
||||
|
||||
// 3. Update component state
|
||||
$this->uploadedFiles[] = [
|
||||
'name' => $file->getClientFilename(),
|
||||
'path' => $savedPath,
|
||||
'size' => $file->getSize(),
|
||||
'uploaded_at' => time()
|
||||
];
|
||||
|
||||
// 4. Dispatch events if needed
|
||||
if ($events) {
|
||||
$events->dispatch('file-uploaded', [
|
||||
'filename' => $file->getClientFilename(),
|
||||
'path' => $savedPath
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. Return updated component data
|
||||
return ComponentData::fromArray([
|
||||
'uploaded_files' => $this->uploadedFiles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate uploaded file
|
||||
*/
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// File type validation
|
||||
$allowedTypes = $this->getAllowedMimeTypes();
|
||||
if (!empty($allowedTypes) && !in_array($file->getClientMediaType(), $allowedTypes)) {
|
||||
$errors[] = "File type {$file->getClientMediaType()} is not allowed";
|
||||
}
|
||||
|
||||
// File size validation
|
||||
$maxSize = $this->getMaxFileSize();
|
||||
if ($file->getSize() > $maxSize) {
|
||||
$errors[] = "File size exceeds maximum allowed size";
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
if (!$this->isValidDocument($file)) {
|
||||
$errors[] = "Invalid document format";
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed MIME types
|
||||
*/
|
||||
public function getAllowedMimeTypes(): array
|
||||
{
|
||||
return [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum file size in bytes
|
||||
*/
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return 10 * 1024 * 1024; // 10MB
|
||||
}
|
||||
|
||||
private function isValidDocument(UploadedFile $file): bool
|
||||
{
|
||||
// Custom validation logic
|
||||
return true;
|
||||
}
|
||||
|
||||
private function saveFile(UploadedFile $file): string
|
||||
{
|
||||
// Save file to storage
|
||||
$uploadDir = '/var/www/storage/uploads';
|
||||
$filename = uniqid() . '_' . $file->getClientFilename();
|
||||
$file->moveTo($uploadDir . '/' . $filename);
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Upload Endpoint
|
||||
|
||||
**Route**: `POST /live-component/{id}/upload`
|
||||
|
||||
**Request Format**:
|
||||
```
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <binary file data>
|
||||
state: <JSON string of current component state>
|
||||
params: <JSON string of additional parameters>
|
||||
_csrf_token: <CSRF token>
|
||||
```
|
||||
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"html": "<updated component HTML>",
|
||||
"state": {
|
||||
"id": "document-upload:abc123",
|
||||
"component": "DocumentUploadComponent",
|
||||
"data": {
|
||||
"uploaded_files": [...]
|
||||
}
|
||||
},
|
||||
"events": [...],
|
||||
"file": {
|
||||
"name": "document.pdf",
|
||||
"size": 1048576,
|
||||
"type": "application/pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "File validation failed",
|
||||
"errors": [
|
||||
"File type application/octet-stream is not allowed"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Quick Start: FileUploadWidget
|
||||
|
||||
Der einfachste Weg, File Uploads zu implementieren, ist die Verwendung des `FileUploadWidget`:
|
||||
|
||||
```html
|
||||
<!-- In your LiveComponent template -->
|
||||
<div data-live-component="document-upload:abc123">
|
||||
<!-- Upload Widget Container -->
|
||||
<div id="upload-widget"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { FileUploadWidget } from '/resources/js/modules/livecomponent/FileUploadWidget.js';
|
||||
|
||||
// Initialize widget
|
||||
const widget = new FileUploadWidget(
|
||||
document.getElementById('upload-widget'),
|
||||
{
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedMimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
|
||||
allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'],
|
||||
maxFiles: 5,
|
||||
multiple: true,
|
||||
autoUpload: true,
|
||||
showPreviews: true,
|
||||
showProgress: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
### Advanced: ComponentFileUploader
|
||||
|
||||
Für vollständige Kontrolle verwenden Sie direkt `ComponentFileUploader`:
|
||||
|
||||
```javascript
|
||||
import { ComponentFileUploader } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const componentElement = document.querySelector('[data-live-id="document-upload:abc123"]');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
// Configuration
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
allowedMimeTypes: ['application/pdf', 'image/jpeg'],
|
||||
maxFiles: 10,
|
||||
autoUpload: true,
|
||||
multiple: true,
|
||||
maxConcurrentUploads: 2,
|
||||
|
||||
// UI Elements
|
||||
dropZone: dropZone,
|
||||
fileInput: fileInput,
|
||||
|
||||
// Callbacks
|
||||
onFileAdded: ({ fileId, file, progress }) => {
|
||||
console.log('File added:', file.name);
|
||||
// Update UI
|
||||
},
|
||||
|
||||
onUploadStart: ({ fileId, file }) => {
|
||||
console.log('Upload started:', file.name);
|
||||
},
|
||||
|
||||
onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => {
|
||||
console.log(`Upload progress: ${percentage}%`);
|
||||
// Update progress bar
|
||||
},
|
||||
|
||||
onUploadComplete: ({ fileId, file, response }) => {
|
||||
console.log('Upload complete:', file.name);
|
||||
// Handle success
|
||||
},
|
||||
|
||||
onUploadError: ({ fileId, file, error }) => {
|
||||
console.error('Upload failed:', error);
|
||||
// Handle error
|
||||
},
|
||||
|
||||
onAllUploadsComplete: ({ totalFiles, successCount, errorCount }) => {
|
||||
console.log(`All uploads complete: ${successCount}/${totalFiles} succeeded`);
|
||||
}
|
||||
});
|
||||
|
||||
// Programmatically add files
|
||||
uploader.addFiles([file1, file2, file3]);
|
||||
|
||||
// Start uploads (if autoUpload is false)
|
||||
uploader.uploadAll();
|
||||
|
||||
// Cancel all uploads
|
||||
uploader.cancelAll();
|
||||
|
||||
// Get statistics
|
||||
const stats = uploader.getStats();
|
||||
console.log(`Progress: ${stats.overallProgress}%`);
|
||||
```
|
||||
|
||||
### Custom Drag & Drop
|
||||
|
||||
```javascript
|
||||
import { DragDropZone } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const dropZone = new DragDropZone(document.getElementById('drop-area'), {
|
||||
onFilesDropped: (files) => {
|
||||
console.log('Files dropped:', files);
|
||||
uploader.addFiles(files);
|
||||
},
|
||||
onDragEnter: () => {
|
||||
console.log('Drag enter');
|
||||
},
|
||||
onDragLeave: () => {
|
||||
console.log('Drag leave');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Validation
|
||||
|
||||
```javascript
|
||||
import { FileValidator } from '/resources/js/modules/livecomponent/ComponentFileUploader.js';
|
||||
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
allowedMimeTypes: ['image/jpeg', 'image/png'],
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png'],
|
||||
minFileSize: 1024 // 1KB minimum
|
||||
});
|
||||
|
||||
// Validate single file
|
||||
const errors = validator.validate(file);
|
||||
if (errors.length > 0) {
|
||||
console.error('Validation errors:', errors);
|
||||
}
|
||||
|
||||
// Quick validation
|
||||
if (!validator.isValid(file)) {
|
||||
console.error('File is not valid');
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### ComponentFileUploader Options
|
||||
|
||||
```javascript
|
||||
{
|
||||
// File Constraints
|
||||
maxFileSize: 10 * 1024 * 1024, // Maximum file size in bytes (default: 10MB)
|
||||
allowedMimeTypes: [], // Array of allowed MIME types (empty = allow all)
|
||||
allowedExtensions: [], // Array of allowed file extensions (empty = allow all)
|
||||
maxFiles: 10, // Maximum number of files (default: 10)
|
||||
|
||||
// Upload Behavior
|
||||
autoUpload: true, // Auto-upload on file add (default: true)
|
||||
multiple: true, // Allow multiple files (default: true)
|
||||
maxConcurrentUploads: 2, // Max concurrent uploads (default: 2)
|
||||
|
||||
// Endpoints
|
||||
endpoint: '/live-component/{id}/upload', // Upload endpoint (default: auto-detected)
|
||||
|
||||
// UI Elements (optional)
|
||||
dropZone: HTMLElement, // Drop zone element
|
||||
fileInput: HTMLElement, // File input element
|
||||
|
||||
// Callbacks
|
||||
onFileAdded: (data) => {}, // Called when file is added
|
||||
onFileRemoved: (data) => {}, // Called when file is removed
|
||||
onUploadStart: (data) => {}, // Called when upload starts
|
||||
onUploadProgress: (data) => {}, // Called during upload
|
||||
onUploadComplete: (data) => {}, // Called on upload success
|
||||
onUploadError: (data) => {}, // Called on upload error
|
||||
onAllUploadsComplete: (data) => {} // Called when all uploads are done
|
||||
}
|
||||
```
|
||||
|
||||
### FileUploadWidget Options
|
||||
|
||||
```javascript
|
||||
{
|
||||
// Inherits all ComponentFileUploader options, plus:
|
||||
|
||||
// UI Configuration
|
||||
showPreviews: true, // Show image previews (default: true)
|
||||
showProgress: true, // Show progress bars (default: true)
|
||||
showFileList: true, // Show file list (default: true)
|
||||
|
||||
// Text Configuration
|
||||
dropZoneText: 'Drag & drop files here or click to browse',
|
||||
browseButtonText: 'Browse Files',
|
||||
uploadButtonText: 'Upload All'
|
||||
}
|
||||
```
|
||||
|
||||
## Callback Data Structures
|
||||
|
||||
### onFileAdded
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File, // Native File object
|
||||
progress: {
|
||||
fileId: 'unique-file-id',
|
||||
fileName: 'document.pdf',
|
||||
fileSize: 1048576,
|
||||
uploadedBytes: 0,
|
||||
percentage: 0,
|
||||
status: 'pending',
|
||||
error: null,
|
||||
uploadSpeed: 0,
|
||||
remainingTime: 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadProgress
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
fileName: 'document.pdf',
|
||||
fileSize: 1048576,
|
||||
uploadedBytes: 524288, // Bytes uploaded so far
|
||||
percentage: 50, // Upload percentage (0-100)
|
||||
status: 'uploading',
|
||||
uploadSpeed: 1048576, // Bytes per second
|
||||
remainingTime: 0.5 // Seconds remaining
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadComplete
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File,
|
||||
response: {
|
||||
success: true,
|
||||
html: '<updated component HTML>',
|
||||
state: {...},
|
||||
events: [...],
|
||||
file: {
|
||||
name: 'document.pdf',
|
||||
size: 1048576,
|
||||
type: 'application/pdf'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### onUploadError
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileId: 'unique-file-id',
|
||||
file: File,
|
||||
error: 'File validation failed'
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases & Examples
|
||||
|
||||
### Basic Image Upload
|
||||
|
||||
```php
|
||||
final class ProfileImageUpload extends AbstractLiveComponent implements SupportsFileUpload
|
||||
{
|
||||
private ?string $profileImage = null;
|
||||
|
||||
public function handleUpload(UploadedFile $file, ?ComponentEventDispatcher $events = null): ComponentData
|
||||
{
|
||||
// Save image
|
||||
$filename = $this->imageService->save($file, 'profiles');
|
||||
|
||||
// Update state
|
||||
$this->profileImage = $filename;
|
||||
|
||||
return ComponentData::fromArray([
|
||||
'profile_image' => $this->profileImage
|
||||
]);
|
||||
}
|
||||
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Only allow images
|
||||
if (!str_starts_with($file->getClientMediaType(), 'image/')) {
|
||||
$errors[] = 'Only images are allowed';
|
||||
}
|
||||
|
||||
// Max 2MB
|
||||
if ($file->getSize() > 2 * 1024 * 1024) {
|
||||
$errors[] = 'Image must be smaller than 2MB';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function getAllowedMimeTypes(): array
|
||||
{
|
||||
return ['image/jpeg', 'image/png', 'image/webp'];
|
||||
}
|
||||
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
return 2 * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Document Upload with Progress
|
||||
|
||||
```javascript
|
||||
import { ComponentFileUploader } from './ComponentFileUploader.js';
|
||||
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxFiles: 20,
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
allowedMimeTypes: [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
],
|
||||
|
||||
onUploadProgress: ({ fileId, percentage, uploadSpeed, remainingTime }) => {
|
||||
// Update progress UI
|
||||
const progressBar = document.querySelector(`[data-file-id="${fileId}"] .progress-bar`);
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
|
||||
const progressText = document.querySelector(`[data-file-id="${fileId}"] .progress-text`);
|
||||
progressText.textContent = `${percentage}% - ${formatSpeed(uploadSpeed)} - ${formatTime(remainingTime)} remaining`;
|
||||
},
|
||||
|
||||
onAllUploadsComplete: ({ successCount, errorCount }) => {
|
||||
if (errorCount === 0) {
|
||||
alert(`All ${successCount} files uploaded successfully!`);
|
||||
} else {
|
||||
alert(`${successCount} files uploaded, ${errorCount} failed`);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Validation Messages
|
||||
|
||||
```javascript
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
allowedMimeTypes: ['application/pdf']
|
||||
});
|
||||
|
||||
const errors = validator.validate(file);
|
||||
|
||||
// Translate errors
|
||||
const translatedErrors = errors.map(error => {
|
||||
if (error.includes('File size')) {
|
||||
return 'Dateigröße überschreitet das Maximum';
|
||||
} else if (error.includes('File type')) {
|
||||
return 'Nur PDF-Dateien sind erlaubt';
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
if (translatedErrors.length > 0) {
|
||||
showErrorMessages(translatedErrors);
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### CSRF Protection
|
||||
|
||||
Das System verwendet automatisch CSRF-Tokens:
|
||||
|
||||
```javascript
|
||||
// CSRF token wird automatisch aus Component-Element gelesen
|
||||
const csrfToken = componentElement.dataset.csrfToken;
|
||||
|
||||
// Und in jedem Upload-Request gesendet
|
||||
xhr.setRequestHeader('X-CSRF-Form-ID', csrfTokens.form_id);
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrfTokens.token);
|
||||
```
|
||||
|
||||
### File Validation
|
||||
|
||||
**Backend Validation ist PFLICHT**:
|
||||
|
||||
```php
|
||||
public function validateUpload(UploadedFile $file): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// 1. MIME type validation
|
||||
$allowedTypes = $this->getAllowedMimeTypes();
|
||||
if (!in_array($file->getClientMediaType(), $allowedTypes)) {
|
||||
$errors[] = 'Invalid file type';
|
||||
}
|
||||
|
||||
// 2. File extension validation
|
||||
$extension = pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
|
||||
if (!in_array(strtolower($extension), ['pdf', 'jpg', 'png'])) {
|
||||
$errors[] = 'Invalid file extension';
|
||||
}
|
||||
|
||||
// 3. File size validation
|
||||
if ($file->getSize() > $this->getMaxFileSize()) {
|
||||
$errors[] = 'File too large';
|
||||
}
|
||||
|
||||
// 4. File content validation (check magic bytes)
|
||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
$actualMimeType = $finfo->file($file->getStream()->getMetadata('uri'));
|
||||
if ($actualMimeType !== $file->getClientMediaType()) {
|
||||
$errors[] = 'File content does not match declared type';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
```
|
||||
|
||||
### Secure File Storage
|
||||
|
||||
```php
|
||||
private function saveFile(UploadedFile $file): string
|
||||
{
|
||||
// 1. Generate secure filename (no user input)
|
||||
$filename = bin2hex(random_bytes(16)) . '.' . $this->getSecureExtension($file);
|
||||
|
||||
// 2. Store outside web root
|
||||
$uploadDir = '/var/www/storage/uploads';
|
||||
|
||||
// 3. Set restrictive permissions
|
||||
$file->moveTo($uploadDir . '/' . $filename);
|
||||
chmod($uploadDir . '/' . $filename, 0600);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
private function getSecureExtension(UploadedFile $file): string
|
||||
{
|
||||
// Use MIME type to determine extension, not user-provided extension
|
||||
return match ($file->getClientMediaType()) {
|
||||
'application/pdf' => 'pdf',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
default => 'bin'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Concurrent Uploads
|
||||
|
||||
```javascript
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxConcurrentUploads: 3, // Upload 3 files simultaneously
|
||||
autoUpload: true
|
||||
});
|
||||
```
|
||||
|
||||
### Chunked Uploads (Large Files)
|
||||
|
||||
Für große Dateien sollten Chunked Uploads implementiert werden:
|
||||
|
||||
```javascript
|
||||
// TODO: Chunked upload support (geplant für v2.0)
|
||||
// Aktuell empfohlen: maxFileSize Limit für große Dateien
|
||||
```
|
||||
|
||||
### Progress Throttling
|
||||
|
||||
```javascript
|
||||
let lastProgressUpdate = 0;
|
||||
|
||||
onUploadProgress: ({ fileId, percentage }) => {
|
||||
const now = Date.now();
|
||||
|
||||
// Only update UI every 100ms
|
||||
if (now - lastProgressUpdate > 100) {
|
||||
updateProgressBar(fileId, percentage);
|
||||
lastProgressUpdate = now;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Upload schlägt fehl mit 413 (Request Entity Too Large)
|
||||
|
||||
**Lösung**: Erhöhe PHP Upload Limits:
|
||||
|
||||
```ini
|
||||
; php.ini
|
||||
upload_max_filesize = 50M
|
||||
post_max_size = 50M
|
||||
```
|
||||
|
||||
### Problem: CSRF Token fehlt
|
||||
|
||||
**Lösung**: Stelle sicher, dass Component CSRF Token hat:
|
||||
|
||||
```html
|
||||
<div data-live-component="upload:123" data-csrf-token="<?= $csrfToken ?>">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### Problem: Uploads sind langsam
|
||||
|
||||
**Lösungen**:
|
||||
1. Erhöhe `maxConcurrentUploads`
|
||||
2. Implementiere Chunked Uploads
|
||||
3. Komprimiere Dateien vor Upload (z.B. Bilder)
|
||||
4. Verwende CDN für statische Assets
|
||||
|
||||
### Problem: Browser hängt bei vielen Dateien
|
||||
|
||||
**Lösung**: Limitiere `maxFiles` und zeige Queue-Status:
|
||||
|
||||
```javascript
|
||||
const uploader = new ComponentFileUploader(componentElement, {
|
||||
maxFiles: 20, // Limit gleichzeitig ausgewählte Dateien
|
||||
maxConcurrentUploads: 2 // Aber nur 2 gleichzeitig hochladen
|
||||
});
|
||||
```
|
||||
|
||||
### Problem: Drag & Drop funktioniert nicht
|
||||
|
||||
**Lösung**: Prüfe Event Listener Setup:
|
||||
|
||||
```javascript
|
||||
// Stelle sicher, dass Drop Zone Element existiert
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
if (!dropZone) {
|
||||
console.error('Drop zone element not found');
|
||||
}
|
||||
|
||||
// Prüfe CSS cursor
|
||||
dropZone.style.cursor = 'pointer';
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Immer Backend-Validierung
|
||||
|
||||
```php
|
||||
// ❌ Niemals nur Frontend-Validierung verlassen
|
||||
// ✅ Immer Backend validateUpload() implementieren
|
||||
```
|
||||
|
||||
### 2. Sichere Dateinamen
|
||||
|
||||
```php
|
||||
// ❌ User-provided filenames verwenden
|
||||
$file->moveTo('/uploads/' . $file->getClientFilename());
|
||||
|
||||
// ✅ Sichere, generierte Dateinamen
|
||||
$file->moveTo('/uploads/' . bin2hex(random_bytes(16)) . '.pdf');
|
||||
```
|
||||
|
||||
### 3. Progress Feedback
|
||||
|
||||
```javascript
|
||||
// ✅ Immer Progress anzeigen für bessere UX
|
||||
onUploadProgress: ({ percentage }) => {
|
||||
updateProgressBar(percentage);
|
||||
updateStatusText(`Uploading: ${percentage}%`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
```javascript
|
||||
// ✅ User-freundliche Fehlermeldungen
|
||||
onUploadError: ({ error }) => {
|
||||
const userMessage = translateError(error);
|
||||
showNotification(userMessage, 'error');
|
||||
logError(error); // Log für Debugging
|
||||
}
|
||||
```
|
||||
|
||||
### 5. File Size Limits
|
||||
|
||||
```php
|
||||
// ✅ Realistische Limits setzen
|
||||
public function getMaxFileSize(): int
|
||||
{
|
||||
// 10MB für Dokumente
|
||||
return 10 * 1024 * 1024;
|
||||
|
||||
// 5MB für Bilder
|
||||
// return 5 * 1024 * 1024;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests (Frontend)
|
||||
|
||||
```javascript
|
||||
import { FileValidator } from './ComponentFileUploader.js';
|
||||
|
||||
describe('FileValidator', () => {
|
||||
it('validates file size', () => {
|
||||
const validator = new FileValidator({
|
||||
maxFileSize: 1024 * 1024 // 1MB
|
||||
});
|
||||
|
||||
const file = new File(['x'.repeat(2 * 1024 * 1024)], 'large.pdf');
|
||||
const errors = validator.validate(file);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('File size');
|
||||
});
|
||||
|
||||
it('validates MIME type', () => {
|
||||
const validator = new FileValidator({
|
||||
allowedMimeTypes: ['application/pdf']
|
||||
});
|
||||
|
||||
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const errors = validator.validate(file);
|
||||
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('not allowed');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests (Backend)
|
||||
|
||||
```php
|
||||
it('handles file upload successfully', function () {
|
||||
$component = new DocumentUploadComponent();
|
||||
|
||||
$file = createUploadedFile('test.pdf', 'application/pdf', 1024);
|
||||
|
||||
$result = $component->handleUpload($file);
|
||||
|
||||
expect($result->toArray())->toHaveKey('uploaded_files');
|
||||
});
|
||||
|
||||
it('validates file type', function () {
|
||||
$component = new DocumentUploadComponent();
|
||||
|
||||
$file = createUploadedFile('test.exe', 'application/octet-stream', 1024);
|
||||
|
||||
$errors = $component->validateUpload($file);
|
||||
|
||||
expect($errors)->not->toBeEmpty();
|
||||
});
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Das File Upload System bietet:
|
||||
|
||||
- ✅ **Einfache Integration** - FileUploadWidget für schnellen Start
|
||||
- ✅ **Flexible API** - ComponentFileUploader für vollständige Kontrolle
|
||||
- ✅ **Drag & Drop** - Intuitive Dateiauswahl
|
||||
- ✅ **Multi-File Support** - Mehrere Dateien gleichzeitig
|
||||
- ✅ **Progress Tracking** - Echtzeit-Fortschrittsanzeige
|
||||
- ✅ **Validation** - Client & Server-Side
|
||||
- ✅ **Security** - CSRF Protection, sichere Dateinamen
|
||||
- ✅ **Performance** - Concurrent Uploads, Queue Management
|
||||
- ✅ **Responsive Design** - Mobile-optimiert
|
||||
- ✅ **Dark Mode** - Automatische Theme-Unterstützung
|
||||
|
||||
**Framework Integration**:
|
||||
- Value Objects für Type Safety
|
||||
- Event System Integration
|
||||
- Component State Management
|
||||
- CSRF Token Handling
|
||||
- Automatic HTML Refresh
|
||||
Reference in New Issue
Block a user