upload volledige repo
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user