# COPY_MOVE_TASKS_DESIGN.md

## Doel
Ontwerpvoorstel voor de volgende implementatieslice: `copy`, `move` en `tasks`.

Dit document bevat alleen ontwerpkeuzes. Er is geen code-implementatie in deze stap.

## 1. Destination-semantiek (expliciet)

V1 kiest een strikte semantiek:
- `destination` is altijd het volledige beoogde eindpad (inclusief bestandsnaam).
- `destination` mag in v1 **niet** geïnterpreteerd worden als "copy/move into existing directory".
- Als `destination` al bestaat (file of directory) -> `already_exists` (409).

Voorbeeld v1:
- source: `storage1/a/file.txt`
- destination: `storage2/b/file_copy.txt`

Niet toegestaan als impliciete directory-target in v1:
- destination: `storage2/b/` met verwachting dat `file.txt` automatisch eronder komt.

Reden:
- voorkomt ambigu gedrag
- eenvoudiger API-contract
- minder regressierisico

## 2. Scopevoorstel `copy` v1

### Keuze
- `copy` ondersteunt in v1 **alleen files**.
- directory-recursie schuift door naar een latere fase.

### Motivatie
- Complexiteit: recursieve copy voegt veel edge-cases toe (diepe bomen, symlinks, partial failures).
- Testbaarheid: file-only maakt golden/regressie tests kleiner en deterministischer.
- Regressierisico: lagere kans op onverwachte performance/security regressies.

### Conflictgedrag
- destination bestaat al -> `already_exists` (409).
- geen overwrite/merge in v1.

### Uitvoering
- altijd task-based (async).
- create response: `task_id` + `queued`.

### Foutmodel copy
- `invalid_request` (400)
- `path_traversal_detected` (403)
- `path_outside_whitelist` (403)
- `invalid_root_alias` (403)
- `path_not_found` (404)
- `type_conflict` (409) (source is geen file)
- `already_exists` (409)
- `io_error` (500)

## 3. Scopevoorstel `move` v1

### Rename vs move
- `rename` blijft naamwijziging binnen dezelfde parent directory.
- `move` is padwijziging naar een ander eindpad (zelfde root of cross-root).

### Cross-root gedrag
- toegestaan als source en destination beide binnen whitelist vallen.
- zelfde root: native rename/move waar mogelijk.
- cross-root: copy+delete binnen dezelfde task.

### Keuze
- `move` ondersteunt in v1 **alleen files**.
- directory-move buiten scope in v1.

### Conflictgedrag
- destination bestaat al -> `already_exists` (409).
- geen overwrite in v1.

### Foutmodel move
- `invalid_request` (400)
- `path_traversal_detected` (403)
- `path_outside_whitelist` (403)
- `invalid_root_alias` (403)
- `path_not_found` (404)
- `type_conflict` (409) (source is geen file)
- `already_exists` (409)
- `io_error` (500)

## 4. Symlinkbeleid (copy/move)

### Source
- v1 file-only: source mag geen symlink zijn.
- als source symlink resolve’t naar pad buiten whitelist -> geblokkeerd (`path_outside_whitelist`).
- als source symlink resolve’t binnen whitelist: in v1 alsnog afwijzen als `type_conflict` om semantiek simpel te houden.

### Destination
- destination wordt via `path_guard` canoniek gevalideerd.
- destination parent moet binnen whitelist liggen.
- destination parent die via symlink buiten whitelist valt -> blokkeren (`path_outside_whitelist`).

### Recursieve escapes
- Niet van toepassing in v1 (geen directory-recursie).
- Voor latere directory-copy geldt: elk bezocht pad moet per entry containment-check krijgen.

## 5. Task persistence en history

### Relatie tasks vs history
- In v1 zijn `tasks` en `history` aparte modellen/tabelrollen:
  - `tasks`: actuele en afgeronde task-status/progress
  - `history`: audit-log van uitgevoerde operaties
- history kan vanuit task-completion gevuld worden, maar is niet hetzelfde model.

### Retentie
- v1 bewaart `completed` en `failed` tasks persistent (geen automatische cleanup in scope).

### Sortering GET /api/tasks
- standaard sortering: `created_at DESC` (nieuwste eerst).

## 6. Taskmodel

### Wanneer task verplicht is
- `copy` en `move` altijd task-based.

### Statussen
- `queued`
- `running`
- `completed`
- `failed`

### Progress
- file-only v1:
  - `done_bytes`
  - `total_bytes`
- `done_items`/`total_items` optioneel en kunnen `null` blijven in v1.

### Partial failure
- fail-fast.
- eerste fatale fout -> `failed`.
- geen rollback in v1.
- taskdetail bevat `failed_item`, `error_code`, `error_message`.

### Duplicate create requests (correctie)
- v1 gedrag is **niet idempotent**.
- twee identieke requests mogen twee verschillende tasks creëren.

## 7. API-voorstel

### Endpoints
- `POST /api/files/copy`
- `POST /api/files/move`
- `GET /api/tasks/{task_id}`
- `GET /api/tasks`

### Request/response shapes

#### POST /api/files/copy
Request:
```json
{
  "source": "storage1/path/file.txt",
  "destination": "storage2/path/file_copy.txt"
}
```
Response (202):
```json
{
  "task_id": "<uuid>",
  "status": "queued"
}
```

#### POST /api/files/move
Request:
```json
{
  "source": "storage1/path/file.txt",
  "destination": "storage2/path/file_moved.txt"
}
```
Response (202):
```json
{
  "task_id": "<uuid>",
  "status": "queued"
}
```

#### GET /api/tasks/{task_id}
Response (200):
```json
{
  "id": "<uuid>",
  "operation": "copy",
  "status": "running",
  "source": "storage1/a/file.txt",
  "destination": "storage2/b/file.txt",
  "done_bytes": 1024,
  "total_bytes": 4096,
  "done_items": null,
  "total_items": null,
  "current_item": "storage1/a/file.txt",
  "failed_item": null,
  "error_code": null,
  "error_message": null,
  "created_at": "2026-03-10T00:00:00Z",
  "started_at": "2026-03-10T00:00:01Z",
  "finished_at": null
}
```

#### GET /api/tasks
Response (200):
```json
{
  "items": [
    {
      "id": "<uuid>",
      "operation": "move",
      "status": "completed",
      "source": "storage1/a/file.txt",
      "destination": "storage2/b/file.txt",
      "created_at": "2026-03-10T00:00:00Z",
      "finished_at": "2026-03-10T00:00:05Z"
    }
  ]
}
```

### Waarom `source` en `destination` ook in task-list
- Ja, opnemen in `GET /api/tasks`.
- Reden: operator kan zonder extra detail-calls direct zien wat elke task doet.
- Dit maakt UI en troubleshooting eenvoudiger.

### Error codes (versmald)
- `invalid_request` (400)
- `path_traversal_detected` (403)
- `path_outside_whitelist` (403)
- `invalid_root_alias` (403)
- `path_not_found` (404)
- `task_not_found` (404)
- `type_conflict` (409)
- `already_exists` (409)
- `io_error` (500)

`internal_error` wordt in v1 niet als aparte publieke code gebruikt; onverwachte fouten worden genormaliseerd naar `io_error` of framework-500.

## 8. Teststrategie

### Golden tests
- `POST /api/files/copy` success (task create shape)
- `POST /api/files/move` success (task create shape)
- `already_exists` voor copy/move
- `type_conflict` als source geen file is
- `invalid_request` voor ongeldige payload
- traversal/root errors voor source en destination
- `GET /api/tasks/{task_id}` shapes voor `queued/running/completed/failed`
- `GET /api/tasks` list shape inclusief `source` en `destination`

### Regressietests
- cross-root move gebruikt copy+delete pad correct
- file copy/move met unicode namen
- grote files progress updates blijven monotone stijging
- duplicate create requests leveren 2 verschillende `task_id`s (niet-idempotent)

### Securitytests
- traversal blokkade op source/destination
- destination outside whitelist blokkade
- invalid root alias blokkade
- symlink source wordt afgewezen
- symlinked destination parent buiten whitelist wordt afgewezen

## Implementatiegrenzen voor volgende stap
- Geen wijziging aan bestaande endpoints:
  - browse
  - mkdir
  - rename
  - delete
- Geen nieuwe dependencies zonder expliciete motivatie.
