# 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
