275 lines
7.4 KiB
Markdown
275 lines
7.4 KiB
Markdown
# 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.
|