# 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": "", "status": "queued" } ``` #### POST /api/files/move Request: ```json { "source": "storage1/path/file.txt", "destination": "storage2/path/file_moved.txt" } ``` Response (202): ```json { "task_id": "", "status": "queued" } ``` #### GET /api/tasks/{task_id} Response (200): ```json { "id": "", "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": "", "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.