# Virtual Storage

Virtual storage is a metadata-driven file management system that decouples the logical identity of a file (a stable UUID) from its physical location on disk. Instead of hard-coding directory paths throughout the application, every file and folder is represented as a row in the `virtual_storage` table. Consumers reference the UUID; the system resolves the path at runtime.

This design enables:

- **Stable references**: A file can be moved or renamed on disk without breaking any report, API response, or UI component that holds its UUID.
- **Gradual migration**: Existing files on disk do not need to be relocated. Legacy path lookups continue to work while the metadata layer is populated incrementally.
- **Unified access control**: The `is_private` flag on a folder or file governs access independently of the filesystem.

---

## Table of Contents

1. [Architecture](#1-architecture)
2. [Database Schema](#2-database-schema)
3. [Core Classes](#3-core-classes)
4. [File Storage Disks](#4-file-storage-disks)
5. [API Endpoints](#5-api-endpoints)
6. [File Lifecycle](#6-file-lifecycle)
7. [Legacy Data Migration](#7-legacy-data-migration)
8. [Jasper Reports Integration](#8-jasper-reports-integration)
9. [Frontend Components](#9-frontend-components)
10. [Configuration Reference](#10-configuration-reference)
11. [Deployment Checklist](#11-deployment-checklist)

---

## 1. Architecture

### Storage Modes

The system supports three modes, controlled by the `FILE_STORAGE_MODE` environment variable:

| Mode      | Behaviour                                                                                                                                |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `virtual` | UUID lookups only. No fallback to legacy paths. Requires all files to have a `virtual_storage` record.                                   |
| `hybrid`  | UUID lookup first; falls back to legacy path search if the virtual entry is missing or its `physical_path` is unresolvable. **Default.** |
| `legacy`  | Direct path lookup only. Virtual storage metadata is ignored entirely.                                                                   |

### High-Level Flow

```
Client Request (UUID)
       │
       ▼
VirtualStorageAdapter::FileResolver()
       │
       ├─── [mode: virtual/hybrid] ──► Query virtual_storage by uuid
       │                                       │
       │                               physical_path found?
       │                               ├── YES ──► Resolve disk + path ──► Return file
       │                               └── NO  ──► (hybrid only) fall through
       │
       └─── [mode: hybrid/legacy] ──► Legacy candidate search across disks ──► Return file
```

### Measurement Picture Integration

`mdb_pic` stores a link to virtual storage via the `virtual_filename` column. This UUID is used as the join key in all report queries and gallery components:

```
mdb_pic.virtual_filename  ──────►  virtual_storage.uuid
mdb_pic.virtual_folder_id ──────►  virtual_storage.uuid  (parent folder)
```

---

## 2. Database Schema

### `virtual_storage` Table

| Column          | Type        | Nullable | Default | Description                                         |
| --------------- | ----------- | -------- | ------- | --------------------------------------------------- |
| `id`            | bigint (PK) | No       | auto    | Internal surrogate key                              |
| `uuid`          | char(36)    | No       | —       | Stable public identifier; unique                    |
| `type`          | varchar     | No       | —       | `'file'` or `'folder'`                              |
| `display_name`  | varchar     | No       | —       | User-facing name                                    |
| `physical_path` | varchar     | Yes      | NULL    | Canonical storage path (see §4 for format)          |
| `parent_uuid`   | char(36)    | Yes      | NULL    | UUID of parent folder; NULL = root                  |
| `legacy_path`   | varchar     | Yes      | NULL    | Original file path before virtual storage           |
| `storage_mode`  | varchar     | No       | `'vs'`  | Origin of the record: `vs`, `ERG`, `WEP`, `library` |
| `is_private`    | tinyint(1)  | No       | `0`     | Restricts public access when `1`                    |
| `created_at`    | timestamp   | Yes      | NULL    | —                                                   |
| `updated_at`    | timestamp   | Yes      | NULL    | —                                                   |

**Indexes on `virtual_storage`:**

- `UNIQUE (uuid)`
- `INDEX (uuid)`
- `INDEX (parent_uuid)`
- `INDEX (legacy_path)`
- `INDEX (storage_mode)`
- `UNIQUE (type, legacy_path, storage_mode)` — prevents duplicate imports
- `INDEX (parent_uuid, display_name)` — folder listing queries
- `INDEX (display_name, type)` — report JOIN by filename

### `mdb_pic` — Added Columns

| Column              | Type         | Nullable | Description                                        |
| ------------------- | ------------ | -------- | -------------------------------------------------- |
| `virtual_folder_id` | char(36)     | Yes      | UUID of the virtual folder this picture belongs to |
| `virtual_filename`  | varchar(255) | Yes      | UUID of the virtual_storage file entry             |

**Indexes on `mdb_pic`:**

- `INDEX (virtual_filename)` — `idx_virtual_filename`

### Migration History

| File                                                                    | Date       | Purpose                                                                                                                                  |
| ----------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `2026_03_04_105822_add_virtual_columns_to_mdb_pic.php`                  | 2026-03-04 | Adds `virtual_folder_id` and `virtual_filename` to `mdb_pic`                                                                             |
| `2026_03_24_111533_create_virtual_storage_table.php`                    | 2026-03-24 | Creates the `virtual_storage` table with core columns                                                                                    |
| `2026_03_31_123506_upgrade_virtual_storage_schema.php`                  | 2026-03-31 | Makes `physical_path` nullable; adds `legacy_path`, `storage_mode`, composite unique constraint, and `(parent_uuid, display_name)` index |
| `2026_04_30_153332_add_index_on_virtual_filename_for_mdb_pic_table.php` | 2026-04-30 | Adds `idx_virtual_filename` on `mdb_pic` and `idx_display_name_type` on `virtual_storage` for report JOIN performance                    |

Run them in the order listed:

```bash
php artisan migrate
```

---

## 3. Core Classes

### `App\Model\VirtualStorage`

Simple Eloquent model for the `virtual_storage` table. All columns listed in §2 are mass-assignable via `$fillable`.

```php
VirtualStorage::where('uuid', $uuid)->where('type', 'file')->first();
```

---

### `App\Services\VirtualStorageAdapter`

The primary service. All file resolution and upload logic lives here.

#### Static Methods

**`FileResolver($value, array $options = []): ?array`**

Resolves a file from a UUID or legacy path value. Returns a payload array or `null`.

```php
$payload = VirtualStorageAdapter::FileResolver($uuid);
// Returns:
// [
//   'uuid'          => string,
//   'disk'          => string,
//   'path'          => string,
//   'absolute_path' => string,
//   'mime'          => string,
//   'extension'     => string,
//   'content'       => string|null,
//   'display_name'  => string,
//   'is_image'      => bool,
//   'source'        => 'virtual'|'legacy',
//   'is_thumb'      => bool,
// ]
```

Relevant `$options` keys:

| Key               | Default  | Description                                                                      |
| ----------------- | -------- | -------------------------------------------------------------------------------- |
| `secondary`       | `null`   | A fallback value to try if `$value` resolves nothing                             |
| `include_content` | `true`   | Whether to load file bytes into `content`                                        |
| `target`          | `'file'` | `'file'` or `'thumb'`                                                            |
| `allow_virtual`   | `true`   | Allow UUID lookup                                                                |
| `allow_legacy`    | `null`   | Allow legacy path fallback; defaults to `true` unless mode is `virtual`          |
| `generate_thumb`  | `true`   | Auto-generate thumbnail if missing                                               |
| `storage_mode`    | `null`   | Override `FILE_STORAGE_MODE` for this call                                       |
| `legacy_sources`  | `null`   | Custom list of `{disk, path}` candidates; replaces the default multi-disk search |

---

**`FileUploadService($folder, $file, array $options = []): ?array`**

Stores a file on disk and creates a `virtual_storage` record. Returns a payload array on success, or `null` on failure.

`$folder` may be a `VirtualStorage` model (folder entry) or a folder UUID string.  
`$file` may be an `UploadedFile` instance or a path string (requires `source_disk` option).

Relevant `$options` keys:

| Key                | Default     | Description                                                                       |
| ------------------ | ----------- | --------------------------------------------------------------------------------- |
| `disk`             | `'htmlpic'` | Target storage disk                                                               |
| `destination_path` | `null`      | Override the computed destination path                                            |
| `legacy_path`      | `null`      | Value to store in `virtual_storage.legacy_path`                                   |
| `storage_mode`     | `null`      | Value to store in `virtual_storage.storage_mode`; inherits from folder if not set |
| `is_private`       | `null`      | Privacy flag; inherits from folder if not set                                     |
| `generate_thumb`   | `true`      | Generate a JPEG thumbnail after upload                                            |
| `dry_run`          | `false`     | Compute payload without writing to disk or database                               |
| `move_source`      | `false`     | Move the source file instead of copying                                           |
| `uuid`             | `null`      | Supply a specific UUID; one is generated if omitted                               |

---

**`qualifyPhysicalPath(string $disk, ?string $path): ?string`**

Converts a disk name and relative path to the canonical `physical_path` format stored in the database.

```php
// 'public/htmlpic/subdir/file.jpg'
VirtualStorageAdapter::qualifyPhysicalPath('htmlpic', 'subdir/file.jpg');
```

---

#### Instance Methods

Used internally by the controllers; these are the named operations exposed via routes:

| Method                                 | Description                                                           |
| -------------------------------------- | --------------------------------------------------------------------- |
| `show($uuid)`                          | Returns an HTTP response with the file content                        |
| `thumb($uuid)`                         | Returns an HTTP response with the thumbnail (generates on first call) |
| `getThumbs($folderUuid)`               | Returns JSON listing of files in a folder                             |
| `createFolder()`                       | Creates a new virtual folder                                          |
| `uploadFile()`                         | Handles a form upload and calls `FileUploadService`                   |
| `delete($uuid)`                        | Deletes a file entry and its physical file                            |
| `rename($uuid, $name)`                 | Renames a file's `display_name`                                       |
| `moveFile($uuid, $targetFolderUuid)`   | Moves a file to a different folder                                    |
| `moveFolder($uuid, $targetParentUuid)` | Moves a folder subtree                                                |

---

### Controllers

| Controller                                                      | Responsibility                                                              |
| --------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `App\Http\Controllers\ConfigController`                         | File download, thumbnail, folder listing, rename, delete, move, bulk-delete |
| `App\Http\Controllers\ConfigControllers\ConfigUploadController` | File upload, new folder creation                                            |
| `App\Http\Controllers\getImageController`                       | Legacy image serving; updated to try virtual resolution first               |

---

## 4. File Storage Disks

Disk definitions are in `config/filesystems.php`. The root paths below are under `storage/app/` on the server.

| Disk         | Root (under `storage/app/`)            | Visibility  | Purpose                       |
| ------------ | -------------------------------------- | ----------- | ----------------------------- |
| `htmlpic`    | `public/htmlpic` (env: `HTMLPIC_ROOT`) | public      | Primary image/file storage    |
| `icons`      | `public/htmlpic/icons`                 | public      | App icons                     |
| `docs`       | `public/docs`                          | public      | Documents                     |
| `crm`        | `public/crm`                           | public      | CRM files                     |
| `virtual`    | `public/virtual`                       | **private** | Private virtual storage files |
| `depFolders` | `public/departmentFolders`             | public      | Department file downloads     |
| `public`     | `public`                               | public      | General public files          |
| `local`      | _(storage root)_                       | —           | Internal Laravel default      |

The `physical_path` column in `virtual_storage` stores the path **relative to `storage/app/`**, prefixed with the disk root. For example, a file on the `htmlpic` disk at relative path `subdir/photo.jpg` is stored as `public/htmlpic/subdir/photo.jpg`.

Thumbnails are always written to the `htmlpic` disk under `vs/thumbs/{uuid}.jpg`.

---

## 5. API Endpoints

All routes below require the `tasko.auth` middleware unless marked otherwise.

| Method | Route                        | Controller Method                   | Auth | Description                                         |
| ------ | ---------------------------- | ----------------------------------- | ---- | --------------------------------------------------- |
| `GET`  | `/config/file/v/{uuid}`      | `ConfigController@show`             | Yes  | Serve file content by UUID                          |
| `GET`  | `/config/thumb/v/{uuid}`     | `ConfigController@thumb`            | Yes  | Serve thumbnail by UUID; generates on first request |
| `POST` | `/config/thumbs/warm`        | `ConfigController@warmThumbs`       | Yes  | Pre-generate thumbnails for a list of UUIDs         |
| `GET`  | `/config/gethtmlpic/{uuid}`  | `ConfigController@gethtmlpic`       | Yes  | List folder contents as JSON                        |
| `POST` | `/htmlpic/upload/{params?}`  | `ConfigUploadController@newhtmlpic` | Yes  | Upload a file to a folder                           |
| `POST` | `/config/file/newhtmlfolder` | `ConfigUploadController@newFolder`  | No\* | Create a new folder                                 |
| `POST` | `/config/deleteFolder/{id}`  | `ConfigController@deleteFolder`     | Yes  | Delete a folder (and its children)                  |
| `POST` | `/config/renameFile/{id}`    | `ConfigController@renameFile`       | Yes  | Rename a file                                       |
| `POST` | `/config/files/bulk-delete`  | `ConfigController@deleteFilesBulk`  | Yes  | Delete multiple files                               |
| `POST` | `/config/files/move`         | `ConfigController@moveFiles`        | Yes  | Move files to a target folder                       |

\*The `newFolder` route does not explicitly declare `tasko.auth` in the route file; verify middleware group coverage in production.

---

## 6. File Lifecycle

### Upload

```
POST /htmlpic/upload/{folder_uuid}
  │
  ▼
ConfigUploadController::newhtmlpic()
  │
  ▼
VirtualStorageAdapter::FileUploadService($folder, $uploadedFile)
  ├── Generate UUID (Str::uuid())
  ├── Resolve destination disk from folder's legacy_path or 'htmlpic'
  ├── Resolve destination path: folder's legacy_path + original filename,
  │   OR 'vs/files/{uuid}.ext' for pure virtual folders
  ├── Store file: Storage::disk($disk)->putFileAs(...)
  ├── Generate thumbnail at vs/thumbs/{uuid}.jpg (Intervention Image)
  └── VirtualStorage::create([uuid, type='file', display_name, physical_path, ...])
  │
  ▼
Return payload: { uuid, disk, physical_path, thumb_path, display_name, ... }
```

### Retrieval

```
GET /config/file/v/{uuid}
  │
  ▼
ConfigController::show($uuid)
  │
  ▼
VirtualStorageAdapter::FileResolver($uuid)
  │
  ├── [virtual / hybrid] Query virtual_storage WHERE uuid = ? AND type = 'file'
  │     ├── Found + physical_path set?
  │     │     ├── Parse disk from physical_path prefix
  │     │     ├── Storage::disk($disk)->exists($path) ?
  │     │     │     ├── YES ──► return payload (source='virtual')
  │     │     │     └── NO  ──► try legacy_path candidates
  │     └── Not found ──► fall through to legacy (if hybrid)
  │
  └── [hybrid / legacy] Walk legacy candidate list across all disks
        └── First match ──► return payload (source='legacy')
  │
  ▼
Response::make($content, 200, ['Content-Type' => $mime, ...])
```

### Thumbnail Retrieval

```
GET /config/thumb/v/{uuid}
  │
  ▼
FileResolver($uuid, ['target' => 'thumb'])
  ├── Check vs/thumbs/{uuid}.jpg on 'htmlpic' disk
  │     └── EXISTS ──► serve cached thumbnail
  ├── Resolve source file
  │     └── SVG? ──► serve SVG directly (no raster conversion)
  └── Generate thumbnail (Intervention Image, 150×150, JPEG 80%)
        ├── storeJpgThumbStatic(absolute_path, 'vs/thumbs/{uuid}.jpg')
        └── serve newly created thumbnail
```

---

## 7. Legacy Data Migration

All four commands operate in **metadata-only mode**: they create `virtual_storage` rows and update `mdb_pic` links but do not copy, move, or delete any files on disk. They are idempotent — safe to run multiple times.

### Recommended Execution Order

```bash
# Step 1 — Import the existing htmlpic folder tree
php artisan virtual:import-legacy-htmlpic

# Step 2 — Import ERG measurement result files and link mdb_pic rows
php artisan virtual:migrate-erg-files

# Step 3 — Import WEP point files
php artisan virtual:migrate-wep-files

# Step 4 — (Optional) Fill any legacy_path gaps left by Step 2
php artisan virtual:backfill-erg-legacy-path
```

---

### `virtual:import-legacy-htmlpic`

**Purpose**: Recursively scans the `htmlpic` disk and creates `virtual_storage` folder and file entries (`storage_mode = 'library'`) mirroring the existing directory structure.

**Options:**

| Option                            | Description                                  |
| --------------------------------- | -------------------------------------------- |
| `--dry-run`                       | Log what would be created without writing    |
| `--skip-private`                  | Skip the `private/` subtree entirely         |
| `--max-files=N`                   | Stop after importing N files (0 = unlimited) |
| `--copy`, `--move`, `--no-thumbs` | Accepted but ignored (metadata-only mode)    |

---

### `virtual:migrate-erg-files`

**Purpose**: Scans the `public/` and `public/docs/` disks for ERG measurement result files. For each file, creates a `virtual_storage` record (`storage_mode = 'ERG'`) and updates `mdb_pic.virtual_filename` with the new UUID.

Skips any file whose `legacy_path` was already imported by a previous run, preventing duplicate records.

**Options:**

| Option                            | Description                 |
| --------------------------------- | --------------------------- |
| `--dry-run`                       | Log changes without writing |
| `--max-files=N`                   | Stop after N files          |
| `--copy`, `--move`, `--no-thumbs` | Accepted but ignored        |

---

### `virtual:migrate-wep-files`

**Purpose**: Reads `wep_values` to find image file paths attached to measurement points. Creates `virtual_storage` file records (`storage_mode = 'WEP'`) for each unique path.

Skips any `legacy_path` already imported by ERG migration to prevent overlap.

**Options:** Same as `virtual:migrate-erg-files`.

---

### `virtual:backfill-erg-legacy-path`

**Purpose**: Repair pass for ERG entries that were created without a `legacy_path`. Joins `virtual_storage` against `mdb_pic` using `uuid = virtual_filename`, then picks the best candidate path from `pic_name` / `file_name`.

**Options:**

| Option      | Description                                |
| ----------- | ------------------------------------------ |
| `--dry-run` | Show changes without writing               |
| `--max=N`   | Stop after updating N rows (0 = unlimited) |

---

### Developer Reset (Non-Production Only)

`virtual:reset-develop` truncates all virtual_storage data and reverts `mdb_pic` links. It exists solely to reset a development environment before re-running the import pipeline. **Do not run in production.**

---

## 8. Jasper Reports Integration

### Standard JOIN Pattern

All Jasper JRXML templates that display measurement images use a `LEFT JOIN` to resolve the file path:

```sql
LEFT JOIN virtual_storage vs
    ON vs.uuid = mdb_pic.virtual_filename
    AND vs.type = 'file'
```

With the join in place, the physical file for Jasper server-side rendering is accessed via:

```
$P{Pfad} + "/public/htmlpic/" + vs.physical_path
```

Where `$P{Pfad}` is the application root path parameter passed to the Jasper report at render time. This is preferred over HTTP fetches to `/config/file/v/{uuid}` because Jasper runs server-side and authenticated HTTP calls add unnecessary overhead and potential host-resolution issues.

### Affected Template Families

The Tagesbericht and Maschinenbericht report families were updated. In each, the primary subreports that display photos (`UBR_pkt_2col.jrxml`, `UBR_maschinenbericht_subreport_pic.jrxml`) join `virtual_storage` and conditionally suppress legacy `tick`/filename placeholder fields when a virtual record exists or when the field indicates a signature (`Sign`, `Unterschrift`).

### Notes for New Report Templates

- Join on `vs.uuid = mdb_pic.virtual_filename` (not on `display_name`).
- Always use `LEFT JOIN` so rows without a virtual entry are not filtered out.
- Use `vs.physical_path` for file access, not `mdb_pic.pic_name`.
- If the report also needs to handle files uploaded before the virtual storage migration ran (i.e., `virtual_filename IS NULL`), add a fallback condition using `mdb_pic.pic_name` with the legacy path.

---

## 9. Frontend Components

All components are in `resources/assets/js/components/stats/`.

| Component           | File                    | Purpose                                                                                                                                            |
| ------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ErgImageGallery`   | `ErgImageGallery.tsx`   | Paginated gallery for ERG measurement images. Redux-connected. Tracks `virtual_filename` UUID per image for thumbnail warm-up and routing.         |
| `Medias`            | `Medias.tsx`            | Wrapper that maps a raw picture array and validates `pic_id`, `pic_name`, `pic_bem` before passing to `ErgImageGallery`.                           |
| `ImageWithComment`  | `ImageWithComment.tsx`  | Single image card. Displays the image via `/config/file/v/{uuid}` (or falls back to `missing.png`), and provides edit, delete, and rotate actions. |
| `ImageDeleteDialog` | `ImageDeleteDialog.tsx` | Material-UI confirmation dialog for deleting a single image.                                                                                       |
| `Comment`           | `Comment.tsx`           | Comment thread with optional file attachment. Triggers an upload that creates a new virtual storage entry.                                         |

### Thumbnail Pre-Warming

Before rendering a gallery, the frontend calls `POST /config/thumbs/warm` with an array of UUIDs. This ensures all thumbnails exist on disk before the `<img>` tags request them, avoiding per-image on-the-fly generation delays.

---

## 10. Configuration Reference

| Variable            | Default                             | Description                                                                                                          |
| ------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `FILE_STORAGE_MODE` | `hybrid`                            | Storage resolution strategy. Values: `legacy`, `hybrid`, `virtual`.                                                  |
| `HTMLPIC_DRIVER`    | `local`                             | Filesystem driver for the `htmlpic` disk.                                                                            |
| `HTMLPIC_ROOT`      | `{storage_path}/app/public/htmlpic` | Absolute root path for the `htmlpic` disk. Override when `htmlpic` is mounted from a different location (e.g., NFS). |

---

## 11. Deployment Checklist

For a **fresh installation**:

1. Set environment variables in `.env` (see §10).
2. Run `php artisan migrate` to create `virtual_storage` and add columns to `mdb_pic`.
3. Run `php artisan storage:link` if not already done.
4. Verify the `htmlpic` disk root exists and is writable.

For an **existing installation** migrating from legacy file paths:

1. Complete steps 1–4 above.
2. Run migration commands in order (see §7):
   ```bash
   php artisan virtual:import-legacy-htmlpic
   php artisan virtual:migrate-erg-files
   php artisan virtual:migrate-wep-files
   php artisan virtual:backfill-erg-legacy-path
   ```
3. Leave `FILE_STORAGE_MODE=hybrid` until all `virtual_filename` values in `mdb_pic` are confirmed populated. Hybrid mode ensures no files disappear during the transition.
4. Once the migration is complete and verified, set `FILE_STORAGE_MODE=virtual` to enforce UUID-only resolution.
