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

- 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:
2025-11-09 14:46:15 +01:00
parent 85c369e846
commit 36ef2a1e2c
1366 changed files with 104925 additions and 28719 deletions

View 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