--- title: "DevPlace REST: Project Virtual Filesystem" description: "The per-project file API: listing the tree, reading and writing text files, uploading binaries, mkdir, move and delete, with the relative POSIX path rules and the traversal rejections that guard them." language: null framework: null category: api_design tags: - devplace - api - http-api - filesystem - projects - file-api - rest - path-validation - security keywords: - devplace GET /projects/{slug}/files list tree - devplace write project file endpoint - devplace upload binary project file - devplace project file move delete mkdir - devplace path traversal rejection relative posix path last_updated: 2026-08-12 difficulty: intermediate version: "DevPlace (devplace.net), documented 2026-08" related: - conventions_and_errors.md - authentication.md - README.md search_priority: normal status: published --- # DevPlace REST: Project Virtual Filesystem Each project carries a full virtual filesystem - directories and files - so a project can hold a complete software project. Reading is public (anyone can browse a project's tree); creating, editing, uploading, moving and deleting require the project owner. Text files are editable inline; binary files are uploaded and served from `/static/uploads/project_files/...`. Paths are relative POSIX paths inside the project (for example `src/main.py`). Parent directories are created automatically on write, upload and mkdir. Paths containing `..`, null bytes or empty segments are rejected. Every endpoint follows the shared [Conventions & Errors](conventions_and_errors.md) (auth, content negotiation, status codes); see [Authentication](authentication.md) for the four ways to sign requests. ## `GET /projects/{project_slug}/files` - List a project's files Return the flat list of files and directories in a project. *Minimal role:* Public **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | **Sample response** ```json { "project": { "uid": "PROJECT_UID", "slug": "PROJECT_SLUG" }, "files": [ { "path": "src/main.py", "name": "main.py", "type": "file", "is_binary": false, "size": 42 } ], "is_owner": false } ``` ## `GET /projects/{project_slug}/files/raw` - Read a project file Return one file's metadata and (for text files) its content. *Minimal role:* Public **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | query | string | yes | Relative file path inside the project. | **Sample response** ```json { "path": "src/main.py", "name": "main.py", "type": "file", "is_binary": false, "mime_type": "text/plain", "size": 42, "url": null, "content": "print('hello')\n" } ``` ## `POST /projects/{project_slug}/files/write` - Write a text file Create or overwrite a text file; parent directories are created automatically. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative file path. | | `content` | form | textarea | yes | Full file content (max 400000 chars). | > Owner only; non-owners get `403`. Invalid paths return `400`. **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/main.py", "type": "file" } } ``` ## `GET /projects/{project_slug}/files/lines` - Read a line range Read a 1-indexed inclusive line range of a text file. Returns lines plus total_lines for targeting edits. *Minimal role:* Public **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | query | string | yes | Relative file path. | | `start` | query | integer | no | First line, 1-indexed (default 1). | | `end` | query | integer | no | Last line inclusive; omit or -1 for end of file. | > Text files only; binary, directory, or missing paths return `404`. **Sample response** ```json { "path": "src/main.py", "start": 1, "end": 2, "total_lines": 2, "lines": [ "import os", "print(os.getcwd())" ], "content": "import os\nprint(os.getcwd())" } ``` ## `POST /projects/{project_slug}/files/replace-lines` - Replace a line range Replace lines start..end (inclusive) with new content; empty content deletes the range. Leaves the rest of the file untouched. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative file path. | | `start` | form | integer | yes | First line to replace (1-indexed). | | `end` | form | integer | yes | Last line to replace (inclusive). | | `content` | form | textarea | no | Replacement text (empty deletes the range). | > Owner only. The preferred way to edit a large file; avoids rewriting the whole file. **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/main.py", "type": "file" } } ``` ## `POST /projects/{project_slug}/files/insert-lines` - Insert lines Insert content before a 1-indexed line. Use at=1 to prepend and at=total_lines+1 to append. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative file path. | | `at` | form | integer | yes | Insert before this 1-indexed line. | | `content` | form | textarea | yes | Text to insert. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/main.py", "type": "file" } } ``` ## `POST /projects/{project_slug}/files/delete-lines` - Delete a line range Delete lines start..end (inclusive) from a text file. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative file path. | | `start` | form | integer | yes | First line to delete (1-indexed). | | `end` | form | integer | yes | Last line to delete (inclusive). | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/main.py", "type": "file" } } ``` ## `POST /projects/{project_slug}/files/append` - Append to a file Append content as new lines at the end of a text file; grow a large file across calls without resending it. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative file path. | | `content` | form | textarea | yes | Text to append. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "log.txt", "type": "file" } } ``` ## `POST /projects/{project_slug}/files/upload` - Upload a file into a project Upload a file into a directory (parents created); text decodes to an editable file, otherwise stored as binary. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `file` | form | file | yes | The file to upload. | | `path` | form | string | no | Target directory, empty for the root. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "assets/logo.png", "type": "file", "is_binary": true } } ``` ## `POST /projects/{project_slug}/files/mkdir` - Create a directory Create a directory and any missing parents. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative directory path. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/components", "type": "dir" } } ``` ## `POST /projects/{project_slug}/files/move` - Move or rename Move or rename a file or directory (and its descendants). *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `from_path` | form | string | yes | Existing path. | | `to_path` | form | string | yes | New path. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/new.py" } } ``` ## `POST /projects/{project_slug}/files/delete` - Delete a file or directory Delete a file, or a directory and everything under it. Project owner or an administrator; soft-deleted (restorable from admin trash). Blocked while the project is read-only. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | form | string | yes | Relative path to delete. | **Sample response** ```json { "ok": true, "redirect": "/projects/PROJECT_SLUG/files", "data": { "path": "src/old.py" } } ``` ## `POST /projects/{project_slug}/files/zip` - Queue a zip of files Archive the whole tree, or a subtree via the path query. Returns the job uid and status URL to poll with /zips/{uid}. *Minimal role:* Public **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `project_slug` | path | string | yes | Project slug or uid. | | `path` | query | string | no | Relative file or directory to archive; empty for the whole project. | **Sample response** ```json { "uid": "ZIP_JOB_UID", "status_url": "/zips/ZIP_JOB_UID" } ```